mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-11-03 03:16:10 -06:00 
			
		
		
		
	Enhancement: system status report sanity check, simpler classifier check, styling updates (#9106)
This commit is contained in:
		@@ -303,12 +303,17 @@ describe('SettingsComponent', () => {
 | 
			
		||||
        redis_error:
 | 
			
		||||
          'Error 61 connecting to localhost:6379. Connection refused.',
 | 
			
		||||
        celery_status: SystemStatusItemStatus.ERROR,
 | 
			
		||||
        celery_url: 'celery@localhost',
 | 
			
		||||
        celery_error: 'Error connecting to celery@localhost',
 | 
			
		||||
        index_status: SystemStatusItemStatus.OK,
 | 
			
		||||
        index_last_modified: new Date().toISOString(),
 | 
			
		||||
        index_error: null,
 | 
			
		||||
        classifier_status: SystemStatusItemStatus.OK,
 | 
			
		||||
        classifier_last_trained: new Date().toISOString(),
 | 
			
		||||
        classifier_error: null,
 | 
			
		||||
        sanity_check_status: SystemStatusItemStatus.ERROR,
 | 
			
		||||
        sanity_check_last_run: new Date().toISOString(),
 | 
			
		||||
        sanity_check_error: 'Error running sanity check.',
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
    jest.spyOn(systemStatusService, 'get').mockReturnValue(of(status))
 | 
			
		||||
@@ -320,6 +325,8 @@ describe('SettingsComponent', () => {
 | 
			
		||||
    component['systemStatus'].database.status = SystemStatusItemStatus.OK
 | 
			
		||||
    component['systemStatus'].tasks.redis_status = SystemStatusItemStatus.OK
 | 
			
		||||
    component['systemStatus'].tasks.celery_status = SystemStatusItemStatus.OK
 | 
			
		||||
    component['systemStatus'].tasks.sanity_check_status =
 | 
			
		||||
      SystemStatusItemStatus.OK
 | 
			
		||||
    expect(component.systemStatusHasErrors).toBeFalsy()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -164,7 +164,10 @@ export class SettingsComponent
 | 
			
		||||
      this.systemStatus.tasks.redis_status === SystemStatusItemStatus.ERROR ||
 | 
			
		||||
      this.systemStatus.tasks.celery_status === SystemStatusItemStatus.ERROR ||
 | 
			
		||||
      this.systemStatus.tasks.index_status === SystemStatusItemStatus.ERROR ||
 | 
			
		||||
      this.systemStatus.tasks.classifier_status === SystemStatusItemStatus.ERROR
 | 
			
		||||
      this.systemStatus.tasks.classifier_status ===
 | 
			
		||||
        SystemStatusItemStatus.ERROR ||
 | 
			
		||||
      this.systemStatus.tasks.sanity_check_status ===
 | 
			
		||||
        SystemStatusItemStatus.ERROR
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,7 @@ import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
 | 
			
		||||
import { routes } from 'src/app/app-routing.module'
 | 
			
		||||
import {
 | 
			
		||||
  PaperlessTask,
 | 
			
		||||
  PaperlessTaskName,
 | 
			
		||||
  PaperlessTaskStatus,
 | 
			
		||||
  PaperlessTaskType,
 | 
			
		||||
} from 'src/app/data/paperless-task'
 | 
			
		||||
@@ -39,7 +40,8 @@ const tasks: PaperlessTask[] = [
 | 
			
		||||
    task_file_name: 'test.pdf',
 | 
			
		||||
    date_created: new Date('2023-03-01T10:26:03.093116Z'),
 | 
			
		||||
    date_done: new Date('2023-03-01T10:26:07.223048Z'),
 | 
			
		||||
    type: PaperlessTaskType.File,
 | 
			
		||||
    type: PaperlessTaskType.Auto,
 | 
			
		||||
    task_name: PaperlessTaskName.ConsumeFile,
 | 
			
		||||
    status: PaperlessTaskStatus.Failed,
 | 
			
		||||
    result: 'test.pd: Not consuming test.pdf: It is a duplicate of test (#100)',
 | 
			
		||||
    acknowledged: false,
 | 
			
		||||
@@ -51,7 +53,8 @@ const tasks: PaperlessTask[] = [
 | 
			
		||||
    task_file_name: '191092.pdf',
 | 
			
		||||
    date_created: new Date('2023-03-01T09:26:03.093116Z'),
 | 
			
		||||
    date_done: new Date('2023-03-01T09:26:07.223048Z'),
 | 
			
		||||
    type: PaperlessTaskType.File,
 | 
			
		||||
    type: PaperlessTaskType.Auto,
 | 
			
		||||
    task_name: PaperlessTaskName.ConsumeFile,
 | 
			
		||||
    status: PaperlessTaskStatus.Failed,
 | 
			
		||||
    result:
 | 
			
		||||
      '191092.pd: Not consuming 191092.pdf: It is a duplicate of 191092 (#311)',
 | 
			
		||||
@@ -64,7 +67,8 @@ const tasks: PaperlessTask[] = [
 | 
			
		||||
    task_file_name: 'Scan Jun 6, 2023 at 3.19 PM.pdf',
 | 
			
		||||
    date_created: new Date('2023-06-06T15:22:05.722323-07:00'),
 | 
			
		||||
    date_done: new Date('2023-06-06T15:22:14.564305-07:00'),
 | 
			
		||||
    type: PaperlessTaskType.File,
 | 
			
		||||
    type: PaperlessTaskType.Auto,
 | 
			
		||||
    task_name: PaperlessTaskName.ConsumeFile,
 | 
			
		||||
    status: PaperlessTaskStatus.Pending,
 | 
			
		||||
    result: null,
 | 
			
		||||
    acknowledged: false,
 | 
			
		||||
@@ -76,7 +80,8 @@ const tasks: PaperlessTask[] = [
 | 
			
		||||
    task_file_name: 'paperless-mail-l4dkg8ir',
 | 
			
		||||
    date_created: new Date('2023-06-04T11:24:32.898089-07:00'),
 | 
			
		||||
    date_done: new Date('2023-06-04T11:24:44.678605-07:00'),
 | 
			
		||||
    type: PaperlessTaskType.File,
 | 
			
		||||
    type: PaperlessTaskType.Auto,
 | 
			
		||||
    task_name: PaperlessTaskName.ConsumeFile,
 | 
			
		||||
    status: PaperlessTaskStatus.Complete,
 | 
			
		||||
    result: 'Success. New document id 422 created',
 | 
			
		||||
    acknowledged: false,
 | 
			
		||||
@@ -88,7 +93,8 @@ const tasks: PaperlessTask[] = [
 | 
			
		||||
    task_file_name: 'onlinePaymentSummary.pdf',
 | 
			
		||||
    date_created: new Date('2023-06-01T13:49:51.631305-07:00'),
 | 
			
		||||
    date_done: new Date('2023-06-01T13:49:54.190220-07:00'),
 | 
			
		||||
    type: PaperlessTaskType.File,
 | 
			
		||||
    type: PaperlessTaskType.Auto,
 | 
			
		||||
    task_name: PaperlessTaskName.ConsumeFile,
 | 
			
		||||
    status: PaperlessTaskStatus.Complete,
 | 
			
		||||
    result: 'Success. New document id 421 created',
 | 
			
		||||
    acknowledged: false,
 | 
			
		||||
@@ -100,7 +106,8 @@ const tasks: PaperlessTask[] = [
 | 
			
		||||
    task_file_name: 'paperless-mail-_rrpmqk6',
 | 
			
		||||
    date_created: new Date('2023-06-07T02:54:35.694916Z'),
 | 
			
		||||
    date_done: null,
 | 
			
		||||
    type: PaperlessTaskType.File,
 | 
			
		||||
    type: PaperlessTaskType.Auto,
 | 
			
		||||
    task_name: PaperlessTaskName.ConsumeFile,
 | 
			
		||||
    status: PaperlessTaskStatus.Started,
 | 
			
		||||
    result: null,
 | 
			
		||||
    acknowledged: false,
 | 
			
		||||
@@ -155,7 +162,9 @@ describe('TasksComponent', () => {
 | 
			
		||||
    jest.useFakeTimers()
 | 
			
		||||
    fixture.detectChanges()
 | 
			
		||||
    httpTestingController
 | 
			
		||||
      .expectOne(`${environment.apiBaseUrl}tasks/`)
 | 
			
		||||
      .expectOne(
 | 
			
		||||
        `${environment.apiBaseUrl}tasks/?task_name=consume_file&acknowledged=false`
 | 
			
		||||
      )
 | 
			
		||||
      .flush(tasks)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<div class="modal-header">
 | 
			
		||||
  <h5 class="modal-title" id="modal-basic-title" i18n>System Status</h5>
 | 
			
		||||
  <h6 class="modal-title" id="modal-basic-title" i18n>System Status</h6>
 | 
			
		||||
  <button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
 | 
			
		||||
</div>
 | 
			
		||||
<div class="modal-body">
 | 
			
		||||
@@ -11,11 +11,11 @@
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  } @else {
 | 
			
		||||
    <div class="row row-cols-1 row-cols-md-3 g-3">
 | 
			
		||||
    <div class="row row-cols-1 row-cols-md-4 g-3">
 | 
			
		||||
      <div class="col">
 | 
			
		||||
        <div class="card bg-light h-100">
 | 
			
		||||
          <div class="card-header">
 | 
			
		||||
            <h5 class="card-title mb-0" i18n>Environment</h5>
 | 
			
		||||
            <h6 class="card-title mb-0" i18n>Environment</h6>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="card-body">
 | 
			
		||||
            <dl class="card-text">
 | 
			
		||||
@@ -38,37 +38,96 @@
 | 
			
		||||
      <div class="col">
 | 
			
		||||
        <div class="card bg-light h-100">
 | 
			
		||||
          <div class="card-header">
 | 
			
		||||
            <h5 class="card-title mb-0" i18n>Database</h5>
 | 
			
		||||
            <h6 class="card-title mb-0" i18n>Database</h6>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="card-body">
 | 
			
		||||
            <dl class="card-text">
 | 
			
		||||
              <dt i18n>Type</dt>
 | 
			
		||||
              <dd>{{status.database.type}}</dd>
 | 
			
		||||
              <dt i18n>Status</dt>
 | 
			
		||||
              <dd class="d-flex align-items-center">
 | 
			
		||||
                {{status.database.status}}
 | 
			
		||||
                @if (status.database.status === 'OK') {
 | 
			
		||||
                  <i-bs name="check-circle-fill" class="text-primary ms-2 lh-1" ngbPopover="{{status.database.url}}" triggers="mouseenter:mouseleave"></i-bs>
 | 
			
		||||
                } @else {
 | 
			
		||||
                  <i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1" ngbPopover="{{status.database.url}}: {{status.database.error}}" triggers="mouseenter:mouseleave"></i-bs>
 | 
			
		||||
                }
 | 
			
		||||
              <dd>
 | 
			
		||||
                <button class="btn btn-sm d-flex align-items-center btn-dark text-uppercase small" [ngbPopover]="databaseStatus" triggers="click mouseenter:mouseleave">
 | 
			
		||||
                  {{status.database.status}}
 | 
			
		||||
                  @if (status.database.status === 'OK') {
 | 
			
		||||
                    <i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
 | 
			
		||||
                  } @else {
 | 
			
		||||
                    <i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1"></i-bs>
 | 
			
		||||
                  }
 | 
			
		||||
                </button>
 | 
			
		||||
                <ng-template #databaseStatus>
 | 
			
		||||
                  @if (status.database.status === 'OK') {
 | 
			
		||||
                    {{status.database.url}}
 | 
			
		||||
                  } @else {
 | 
			
		||||
                    {{status.database.url}}: {{status.database.error}}
 | 
			
		||||
                  }
 | 
			
		||||
                </ng-template>
 | 
			
		||||
              </dd>
 | 
			
		||||
              <dt i18n>Migration Status</dt>
 | 
			
		||||
              <dd class="d-flex align-items-center">
 | 
			
		||||
                @if (status.database.migration_status.unapplied_migrations.length === 0) {
 | 
			
		||||
                  <ng-container i18n>Up to date</ng-container><i-bs name="check-circle-fill" class="text-primary ms-2 lh-1" [ngbPopover]="migrationStatus" triggers="mouseenter:mouseleave"></i-bs>
 | 
			
		||||
                } @else {
 | 
			
		||||
                  <ng-container>{{status.database.migration_status.unapplied_migrations.length}} Pending</ng-container><i-bs name="exclamation-triangle-fill" class="text-warning ms-2 lh-1" [ngbPopover]="migrationStatus" triggers="mouseenter:mouseleave"></i-bs>
 | 
			
		||||
                }
 | 
			
		||||
                <ng-template #migrationStatus>
 | 
			
		||||
                  <h6><ng-container i18n>Latest Migration</ng-container>:</h6> <span class="font-monospace small">{{status.database.migration_status.latest_migration}}</span>
 | 
			
		||||
                  @if (status.database.migration_status.unapplied_migrations.length > 0) {
 | 
			
		||||
                    <h6 class="mt-3"><ng-container i18n>Pending Migrations</ng-container>:</h6>
 | 
			
		||||
                    <ul>
 | 
			
		||||
                      @for (migration of status.database.migration_status.unapplied_migrations; track migration) {
 | 
			
		||||
                        <li class="font-monospace small">{{migration}}</li>
 | 
			
		||||
                      }
 | 
			
		||||
                    </ul>
 | 
			
		||||
              <dd>
 | 
			
		||||
                <button class="btn btn-sm d-flex align-items-center btn-dark text-uppercase small" [ngbPopover]="migrationStatus" triggers="click mouseenter:mouseleave">
 | 
			
		||||
                  @if (status.database.migration_status.unapplied_migrations.length === 0) {
 | 
			
		||||
                    <ng-container i18n>Up to date</ng-container><i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
 | 
			
		||||
                  } @else {
 | 
			
		||||
                    <ng-container>{{status.database.migration_status.unapplied_migrations.length}} Pending</ng-container><i-bs name="exclamation-triangle-fill" class="text-warning ms-2 lh-1"></i-bs>
 | 
			
		||||
                  }
 | 
			
		||||
                  <ng-template #migrationStatus>
 | 
			
		||||
                    <h6><ng-container i18n>Latest Migration</ng-container>:</h6> <span class="font-monospace small">{{status.database.migration_status.latest_migration}}</span>
 | 
			
		||||
                    @if (status.database.migration_status.unapplied_migrations.length > 0) {
 | 
			
		||||
                      <h6 class="mt-3"><ng-container i18n>Pending Migrations</ng-container>:</h6>
 | 
			
		||||
                      <ul>
 | 
			
		||||
                        @for (migration of status.database.migration_status.unapplied_migrations; track migration) {
 | 
			
		||||
                          <li class="font-monospace small">{{migration}}</li>
 | 
			
		||||
                        }
 | 
			
		||||
                      </ul>
 | 
			
		||||
                    }
 | 
			
		||||
                  </ng-template>
 | 
			
		||||
                </button>
 | 
			
		||||
              </dd>
 | 
			
		||||
            </dl>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="col">
 | 
			
		||||
        <div class="card bg-light h-100">
 | 
			
		||||
          <div class="card-header">
 | 
			
		||||
            <h6 class="card-title mb-0" i18n>Tasks Queue</h6>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="card-body">
 | 
			
		||||
            <dl class="card-text">
 | 
			
		||||
              <dt i18n>Redis Status</dt>
 | 
			
		||||
              <dd>
 | 
			
		||||
                <button class="btn btn-sm d-flex align-items-center btn-dark text-uppercase small" [ngbPopover]="redisStatus" triggers="click mouseenter:mouseleave">
 | 
			
		||||
                  {{status.tasks.redis_status}}
 | 
			
		||||
                  @if (status.tasks.redis_status === 'OK') {
 | 
			
		||||
                    <i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
 | 
			
		||||
                  } @else {
 | 
			
		||||
                    <i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1"></i-bs>
 | 
			
		||||
                  }
 | 
			
		||||
                </button>
 | 
			
		||||
                <ng-template #redisStatus>
 | 
			
		||||
                  @if (status.tasks.redis_status === 'OK') {
 | 
			
		||||
                    {{status.tasks.redis_url}}
 | 
			
		||||
                  } @else {
 | 
			
		||||
                    {{status.tasks.redis_url}}: {{status.tasks.redis_error}}
 | 
			
		||||
                  }
 | 
			
		||||
                </ng-template>
 | 
			
		||||
              </dd>
 | 
			
		||||
              <dt i18n>Celery Status</dt>
 | 
			
		||||
              <dd>
 | 
			
		||||
                <button class="btn btn-sm d-flex align-items-center btn-dark text-uppercase small" [ngbPopover]="celeryStatus" triggers="click mouseenter:mouseleave">
 | 
			
		||||
                  {{status.tasks.celery_status}}
 | 
			
		||||
                  @if (status.tasks.celery_status === 'OK') {
 | 
			
		||||
                    <i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
 | 
			
		||||
                  } @else {
 | 
			
		||||
                    <i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1"></i-bs>
 | 
			
		||||
                  }
 | 
			
		||||
                </button>
 | 
			
		||||
                <ng-template #celeryStatus>
 | 
			
		||||
                  @if (status.tasks.celery_status === 'OK') {
 | 
			
		||||
                    {{status.tasks.celery_url}}
 | 
			
		||||
                  } @else {
 | 
			
		||||
                    {{status.tasks.celery_error}}
 | 
			
		||||
                  }
 | 
			
		||||
                </ng-template>
 | 
			
		||||
              </dd>
 | 
			
		||||
@@ -80,63 +139,109 @@
 | 
			
		||||
      <div class="col">
 | 
			
		||||
        <div class="card bg-light h-100">
 | 
			
		||||
          <div class="card-header">
 | 
			
		||||
            <h5 class="card-title mb-0" i18n>Tasks</h5>
 | 
			
		||||
            <h6 class="card-title mb-0" i18n>Health</h6>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="card-body">
 | 
			
		||||
            <dl class="card-text">
 | 
			
		||||
              <dt i18n>Redis Status</dt>
 | 
			
		||||
              <dd class="d-flex align-items-center">
 | 
			
		||||
                {{status.tasks.redis_status}}
 | 
			
		||||
                @if (status.tasks.redis_status === 'OK') {
 | 
			
		||||
                  <i-bs name="check-circle-fill" class="text-primary ms-2 lh-1" ngbPopover="{{status.tasks.redis_url}}" triggers="mouseenter:mouseleave"></i-bs>
 | 
			
		||||
                } @else {
 | 
			
		||||
                  <i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1" ngbPopover="{{status.tasks.redis_url}}: {{status.tasks.redis_error}}" triggers="mouseenter:mouseleave"></i-bs>
 | 
			
		||||
                }
 | 
			
		||||
              </dd>
 | 
			
		||||
              <dt i18n>Celery Status</dt>
 | 
			
		||||
              <dd class="d-flex align-items-center">
 | 
			
		||||
                {{status.tasks.celery_status}}
 | 
			
		||||
                @if (status.tasks.celery_status === 'OK') {
 | 
			
		||||
                  <i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
 | 
			
		||||
                } @else {
 | 
			
		||||
                  <i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1"></i-bs>
 | 
			
		||||
                }
 | 
			
		||||
              </dd>
 | 
			
		||||
              <dt i18n>Search Index</dt>
 | 
			
		||||
              <dd class="d-flex align-items-center">
 | 
			
		||||
                {{status.tasks.index_status}}
 | 
			
		||||
                @if (status.tasks.index_status === 'OK') {
 | 
			
		||||
                  @if (isStale(status.tasks.index_last_modified)) {
 | 
			
		||||
                    <i-bs name="exclamation-triangle-fill" class="text-warning ms-2 lh-1" [ngbPopover]="indexStatus" triggers="mouseenter:mouseleave"></i-bs>
 | 
			
		||||
                <button class="btn btn-sm d-flex align-items-center btn-dark text-uppercase small" [ngbPopover]="indexStatus" triggers="click mouseenter:mouseleave">
 | 
			
		||||
                  {{status.tasks.index_status}}
 | 
			
		||||
                  @if (status.tasks.index_status === 'OK') {
 | 
			
		||||
                    @if (isStale(status.tasks.index_last_modified)) {
 | 
			
		||||
                      <i-bs name="exclamation-triangle-fill" class="text-warning ms-2 lh-1"></i-bs>
 | 
			
		||||
                    } @else {
 | 
			
		||||
                      <i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
 | 
			
		||||
                    }
 | 
			
		||||
                  } @else {
 | 
			
		||||
                    <i-bs name="check-circle-fill" class="text-primary ms-2 lh-1" [ngbPopover]="indexStatus" triggers="mouseenter:mouseleave"></i-bs>
 | 
			
		||||
                    <i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1"></i-bs>
 | 
			
		||||
                  }
 | 
			
		||||
                </button>
 | 
			
		||||
                @if (currentUserIsSuperUser) {
 | 
			
		||||
                  @if (isRunning(PaperlessTaskName.IndexOptimize)) {
 | 
			
		||||
                    <div class="spinner-border spinner-border-sm ms-2" role="status"></div>
 | 
			
		||||
                  } @else {
 | 
			
		||||
                    <button class="btn btn-sm d-flex align-items-center btn-dark small ms-2" (click)="runTask(PaperlessTaskName.IndexOptimize)">
 | 
			
		||||
                      <i-bs name="play-fill"></i-bs> 
 | 
			
		||||
                      <ng-container i18n>Run Task</ng-container>
 | 
			
		||||
                    </button>
 | 
			
		||||
                  }
 | 
			
		||||
                } @else {
 | 
			
		||||
                  <i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1" ngbPopover="{{status.tasks.index_error}}" triggers="mouseenter:mouseleave"></i-bs>
 | 
			
		||||
                }
 | 
			
		||||
              </dd>
 | 
			
		||||
              <ng-template #indexStatus>
 | 
			
		||||
                <h6><ng-container i18n>Last Updated</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.index_last_modified | customDate:'medium'}}</span>
 | 
			
		||||
                @if (status.tasks.index_status === 'OK') {
 | 
			
		||||
                  <h6><ng-container i18n>Last Updated</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.index_last_modified | customDate:'medium'}}</span>
 | 
			
		||||
                } @else {
 | 
			
		||||
                  <h6><ng-container i18n>Error</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.index_error}}</span>
 | 
			
		||||
                }
 | 
			
		||||
              </ng-template>
 | 
			
		||||
              <dt i18n>Classifier</dt>
 | 
			
		||||
              <dd class="d-flex align-items-center">
 | 
			
		||||
                {{status.tasks.classifier_status}}
 | 
			
		||||
                @if (status.tasks.classifier_status === 'OK') {
 | 
			
		||||
                  @if (isStale(status.tasks.classifier_last_trained)) {
 | 
			
		||||
                    <i-bs name="exclamation-triangle-fill" class="text-warning ms-2 lh-1" [ngbPopover]="classifierStatus" triggers="mouseenter:mouseleave"></i-bs>
 | 
			
		||||
                <button class="btn btn-sm d-flex align-items-center btn-dark text-uppercase small" [ngbPopover]="classifierStatus" triggers="click mouseenter:mouseleave">
 | 
			
		||||
                  {{status.tasks.classifier_status}}
 | 
			
		||||
                  @if (status.tasks.classifier_status === 'OK') {
 | 
			
		||||
                    @if (isStale(status.tasks.classifier_last_trained)) {
 | 
			
		||||
                      <i-bs name="exclamation-triangle-fill" class="text-warning ms-2 lh-1"></i-bs>
 | 
			
		||||
                    } @else {
 | 
			
		||||
                      <i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
 | 
			
		||||
                    }
 | 
			
		||||
                  } @else {
 | 
			
		||||
                    <i-bs name="check-circle-fill" class="text-primary ms-2 lh-1" [ngbPopover]="classifierStatus" triggers="mouseenter:mouseleave"></i-bs>
 | 
			
		||||
                  }
 | 
			
		||||
                } @else {
 | 
			
		||||
                    <i-bs name="exclamation-triangle-fill" class="ms-2 lh-1"
 | 
			
		||||
                    [class.text-danger]="status.tasks.classifier_status === SystemStatusItemStatus.ERROR"
 | 
			
		||||
                    [class.text-warning]="status.tasks.classifier_status === SystemStatusItemStatus.WARNING"
 | 
			
		||||
                    ngbPopover="{{status.tasks.classifier_error}}"
 | 
			
		||||
                    triggers="mouseenter:mouseleave"></i-bs>
 | 
			
		||||
                    [class.text-warning]="status.tasks.classifier_status === SystemStatusItemStatus.WARNING"></i-bs>
 | 
			
		||||
                  }
 | 
			
		||||
                </button>
 | 
			
		||||
                @if (currentUserIsSuperUser) {
 | 
			
		||||
                  @if (isRunning(PaperlessTaskName.TrainClassifier)) {
 | 
			
		||||
                    <div class="spinner-border spinner-border-sm ms-2" role="status"></div>
 | 
			
		||||
                  } @else {
 | 
			
		||||
                    <button class="btn btn-sm d-flex align-items-center btn-dark small ms-2" (click)="runTask(PaperlessTaskName.TrainClassifier)">
 | 
			
		||||
                      <i-bs name="play-fill"></i-bs> 
 | 
			
		||||
                      <ng-container i18n>Run Task</ng-container>
 | 
			
		||||
                    </button>
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              </dd>
 | 
			
		||||
              <ng-template #classifierStatus>
 | 
			
		||||
                <h6><ng-container i18n>Last Trained</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.classifier_last_trained | customDate:'medium'}}</span>
 | 
			
		||||
                @if (status.tasks.classifier_status === 'OK') {
 | 
			
		||||
                  <h6><ng-container i18n>Last Trained</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.classifier_last_trained | customDate:'medium'}}</span>
 | 
			
		||||
                } @else {
 | 
			
		||||
                  <h6><ng-container i18n>Error</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.classifier_error}}</span>
 | 
			
		||||
                }
 | 
			
		||||
              </ng-template>
 | 
			
		||||
              <dt i18n>Sanity Checker</dt>
 | 
			
		||||
              <dd class="d-flex align-items-center">
 | 
			
		||||
                <button class="btn btn-sm d-flex align-items-center btn-dark text-uppercase small" [ngbPopover]="sanityCheckerStatus" triggers="click mouseenter:mouseleave">
 | 
			
		||||
                  {{status.tasks.sanity_check_status}}
 | 
			
		||||
                  @if (status.tasks.sanity_check_status === 'OK') {
 | 
			
		||||
                    @if (isStale(status.tasks.sanity_check_last_run)) {
 | 
			
		||||
                      <i-bs name="exclamation-triangle-fill" class="text-warning ms-2 lh-1"></i-bs>
 | 
			
		||||
                    } @else {
 | 
			
		||||
                      <i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
 | 
			
		||||
                    }
 | 
			
		||||
                  } @else {
 | 
			
		||||
                    <i-bs name="exclamation-triangle-fill" class="ms-2 lh-1"
 | 
			
		||||
                    [class.text-danger]="status.tasks.sanity_check_status === SystemStatusItemStatus.ERROR"
 | 
			
		||||
                    [class.text-warning]="status.tasks.sanity_check_status === SystemStatusItemStatus.WARNING"></i-bs>
 | 
			
		||||
                  }
 | 
			
		||||
                </button>
 | 
			
		||||
                @if (currentUserIsSuperUser) {
 | 
			
		||||
                  @if (isRunning(PaperlessTaskName.SanityCheck)) {
 | 
			
		||||
                    <div class="spinner-border spinner-border-sm ms-2" role="status"></div>
 | 
			
		||||
                  } @else {
 | 
			
		||||
                    <button class="btn btn-sm d-flex align-items-center btn-dark small ms-2" (click)="runTask(PaperlessTaskName.SanityCheck)">
 | 
			
		||||
                      <i-bs name="play-fill"></i-bs> 
 | 
			
		||||
                      <ng-container i18n>Run Task</ng-container>
 | 
			
		||||
                    </button>
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              </dd>
 | 
			
		||||
              <ng-template #sanityCheckerStatus>
 | 
			
		||||
                @if (status.tasks.sanity_check_status === 'OK') {
 | 
			
		||||
                  <h6><ng-container i18n>Last Run</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.sanity_check_last_run | customDate:'medium'}}</span>
 | 
			
		||||
                } @else {
 | 
			
		||||
                  <h6><ng-container i18n>Error</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.sanity_check_error}}</span>
 | 
			
		||||
                }
 | 
			
		||||
              </ng-template>
 | 
			
		||||
            </dl>
 | 
			
		||||
          </div>
 | 
			
		||||
@@ -146,7 +251,7 @@
 | 
			
		||||
  }
 | 
			
		||||
</div>
 | 
			
		||||
<div class="modal-footer">
 | 
			
		||||
  <button class="btn btn-sm btn-outline-secondary" (click)="copy()">
 | 
			
		||||
  <button class="btn btn-sm d-flex align-items-center btn-dark btn btn-sm d-flex align-items-center btn-dark btn-outline-secondary" (click)="copy()">
 | 
			
		||||
    @if (!copied) {
 | 
			
		||||
      <i-bs name="clipboard-fill"></i-bs> 
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,3 @@
 | 
			
		||||
.btn.small {
 | 
			
		||||
  font-size: 0.75rem;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,11 +9,16 @@ import {
 | 
			
		||||
} from '@angular/core/testing'
 | 
			
		||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
 | 
			
		||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
 | 
			
		||||
import { of, throwError } from 'rxjs'
 | 
			
		||||
import { PaperlessTaskName } from 'src/app/data/paperless-task'
 | 
			
		||||
import {
 | 
			
		||||
  InstallType,
 | 
			
		||||
  SystemStatus,
 | 
			
		||||
  SystemStatusItemStatus,
 | 
			
		||||
} from 'src/app/data/system-status'
 | 
			
		||||
import { SystemStatusService } from 'src/app/services/system-status.service'
 | 
			
		||||
import { TasksService } from 'src/app/services/tasks.service'
 | 
			
		||||
import { ToastService } from 'src/app/services/toast.service'
 | 
			
		||||
import { SystemStatusDialogComponent } from './system-status-dialog.component'
 | 
			
		||||
 | 
			
		||||
const status: SystemStatus = {
 | 
			
		||||
@@ -36,12 +41,17 @@ const status: SystemStatus = {
 | 
			
		||||
    redis_status: SystemStatusItemStatus.ERROR,
 | 
			
		||||
    redis_error: 'Error 61 connecting to localhost:6379. Connection refused.',
 | 
			
		||||
    celery_status: SystemStatusItemStatus.ERROR,
 | 
			
		||||
    celery_url: 'celery@localhost',
 | 
			
		||||
    celery_error: 'Error connecting to celery@localhost',
 | 
			
		||||
    index_status: SystemStatusItemStatus.OK,
 | 
			
		||||
    index_last_modified: new Date().toISOString(),
 | 
			
		||||
    index_error: null,
 | 
			
		||||
    classifier_status: SystemStatusItemStatus.OK,
 | 
			
		||||
    classifier_last_trained: new Date().toISOString(),
 | 
			
		||||
    classifier_error: null,
 | 
			
		||||
    sanity_check_status: SystemStatusItemStatus.OK,
 | 
			
		||||
    sanity_check_last_run: new Date().toISOString(),
 | 
			
		||||
    sanity_check_error: null,
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -49,6 +59,9 @@ describe('SystemStatusDialogComponent', () => {
 | 
			
		||||
  let component: SystemStatusDialogComponent
 | 
			
		||||
  let fixture: ComponentFixture<SystemStatusDialogComponent>
 | 
			
		||||
  let clipboard: Clipboard
 | 
			
		||||
  let tasksService: TasksService
 | 
			
		||||
  let systemStatusService: SystemStatusService
 | 
			
		||||
  let toastService: ToastService
 | 
			
		||||
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    await TestBed.configureTestingModule({
 | 
			
		||||
@@ -67,6 +80,9 @@ describe('SystemStatusDialogComponent', () => {
 | 
			
		||||
    component = fixture.componentInstance
 | 
			
		||||
    component.status = status
 | 
			
		||||
    clipboard = TestBed.inject(Clipboard)
 | 
			
		||||
    tasksService = TestBed.inject(TasksService)
 | 
			
		||||
    systemStatusService = TestBed.inject(SystemStatusService)
 | 
			
		||||
    toastService = TestBed.inject(ToastService)
 | 
			
		||||
    fixture.detectChanges()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
@@ -93,4 +109,37 @@ describe('SystemStatusDialogComponent', () => {
 | 
			
		||||
    expect(component.isStale(date.toISOString())).toBeTruthy()
 | 
			
		||||
    expect(component.isStale(date.toISOString(), 26)).toBeFalsy()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should check if task is running', () => {
 | 
			
		||||
    component.runTask(PaperlessTaskName.IndexOptimize)
 | 
			
		||||
    expect(component.isRunning(PaperlessTaskName.IndexOptimize)).toBeTruthy()
 | 
			
		||||
    expect(component.isRunning(PaperlessTaskName.SanityCheck)).toBeFalsy()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should support running tasks, refresh status and show toasts', () => {
 | 
			
		||||
    const toastSpy = jest.spyOn(toastService, 'showInfo')
 | 
			
		||||
    const toastErrorSpy = jest.spyOn(toastService, 'showError')
 | 
			
		||||
    const getStatusSpy = jest.spyOn(systemStatusService, 'get')
 | 
			
		||||
    const runSpy = jest.spyOn(tasksService, 'run')
 | 
			
		||||
 | 
			
		||||
    // fail first
 | 
			
		||||
    runSpy.mockReturnValue(throwError(() => new Error('error')))
 | 
			
		||||
    component.runTask(PaperlessTaskName.IndexOptimize)
 | 
			
		||||
    expect(runSpy).toHaveBeenCalledWith(PaperlessTaskName.IndexOptimize)
 | 
			
		||||
    expect(toastErrorSpy).toHaveBeenCalledWith(
 | 
			
		||||
      `Failed to start task ${PaperlessTaskName.IndexOptimize}, see the logs for more details`,
 | 
			
		||||
      expect.any(Error)
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    // succeed
 | 
			
		||||
    runSpy.mockReturnValue(of({}))
 | 
			
		||||
    getStatusSpy.mockReturnValue(of(status))
 | 
			
		||||
    component.runTask(PaperlessTaskName.IndexOptimize)
 | 
			
		||||
    expect(runSpy).toHaveBeenCalledWith(PaperlessTaskName.IndexOptimize)
 | 
			
		||||
 | 
			
		||||
    expect(getStatusSpy).toHaveBeenCalled()
 | 
			
		||||
    expect(toastSpy).toHaveBeenCalledWith(
 | 
			
		||||
      `Task ${PaperlessTaskName.IndexOptimize} started`
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -7,12 +7,17 @@ import {
 | 
			
		||||
  NgbProgressbarModule,
 | 
			
		||||
} from '@ng-bootstrap/ng-bootstrap'
 | 
			
		||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
 | 
			
		||||
import { PaperlessTaskName } from 'src/app/data/paperless-task'
 | 
			
		||||
import {
 | 
			
		||||
  SystemStatus,
 | 
			
		||||
  SystemStatusItemStatus,
 | 
			
		||||
} from 'src/app/data/system-status'
 | 
			
		||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
 | 
			
		||||
import { FileSizePipe } from 'src/app/pipes/file-size.pipe'
 | 
			
		||||
import { PermissionsService } from 'src/app/services/permissions.service'
 | 
			
		||||
import { SystemStatusService } from 'src/app/services/system-status.service'
 | 
			
		||||
import { TasksService } from 'src/app/services/tasks.service'
 | 
			
		||||
import { ToastService } from 'src/app/services/toast.service'
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'pngx-system-status-dialog',
 | 
			
		||||
@@ -30,13 +35,24 @@ import { FileSizePipe } from 'src/app/pipes/file-size.pipe'
 | 
			
		||||
})
 | 
			
		||||
export class SystemStatusDialogComponent {
 | 
			
		||||
  public SystemStatusItemStatus = SystemStatusItemStatus
 | 
			
		||||
  public PaperlessTaskName = PaperlessTaskName
 | 
			
		||||
  public status: SystemStatus
 | 
			
		||||
 | 
			
		||||
  public copied: boolean = false
 | 
			
		||||
 | 
			
		||||
  private runningTasks: Set<PaperlessTaskName> = new Set()
 | 
			
		||||
 | 
			
		||||
  get currentUserIsSuperUser(): boolean {
 | 
			
		||||
    return this.permissionsService.isSuperUser()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    public activeModal: NgbActiveModal,
 | 
			
		||||
    private clipboard: Clipboard
 | 
			
		||||
    private clipboard: Clipboard,
 | 
			
		||||
    private systemStatusService: SystemStatusService,
 | 
			
		||||
    private tasksService: TasksService,
 | 
			
		||||
    private toastService: ToastService,
 | 
			
		||||
    private permissionsService: PermissionsService
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  public close() {
 | 
			
		||||
@@ -56,4 +72,30 @@ export class SystemStatusDialogComponent {
 | 
			
		||||
    const now = new Date()
 | 
			
		||||
    return now.getTime() - date.getTime() > hours * 60 * 60 * 1000
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public isRunning(taskName: PaperlessTaskName): boolean {
 | 
			
		||||
    return this.runningTasks.has(taskName)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public runTask(taskName: PaperlessTaskName) {
 | 
			
		||||
    this.runningTasks.add(taskName)
 | 
			
		||||
    this.toastService.showInfo(`Task ${taskName} started`)
 | 
			
		||||
    this.tasksService.run(taskName).subscribe({
 | 
			
		||||
      next: () => {
 | 
			
		||||
        this.runningTasks.delete(taskName)
 | 
			
		||||
        this.systemStatusService.get().subscribe({
 | 
			
		||||
          next: (status) => {
 | 
			
		||||
            this.status = status
 | 
			
		||||
          },
 | 
			
		||||
        })
 | 
			
		||||
      },
 | 
			
		||||
      error: (err) => {
 | 
			
		||||
        this.runningTasks.delete(taskName)
 | 
			
		||||
        this.toastService.showError(
 | 
			
		||||
          `Failed to start task ${taskName}, see the logs for more details`,
 | 
			
		||||
          err
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,16 @@
 | 
			
		||||
import { ObjectWithId } from './object-with-id'
 | 
			
		||||
 | 
			
		||||
export enum PaperlessTaskType {
 | 
			
		||||
  // just file tasks, for now
 | 
			
		||||
  File = 'file',
 | 
			
		||||
  Auto = 'auto_task',
 | 
			
		||||
  ScheduledTask = 'scheduled_task',
 | 
			
		||||
  ManualTask = 'manual_task',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum PaperlessTaskName {
 | 
			
		||||
  ConsumeFile = 'consume_file',
 | 
			
		||||
  TrainClassifier = 'train_classifier',
 | 
			
		||||
  SanityCheck = 'check_sanity',
 | 
			
		||||
  IndexOptimize = 'index_optimize',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum PaperlessTaskStatus {
 | 
			
		||||
@@ -23,6 +31,8 @@ export interface PaperlessTask extends ObjectWithId {
 | 
			
		||||
 | 
			
		||||
  task_file_name: string
 | 
			
		||||
 | 
			
		||||
  task_name: PaperlessTaskName
 | 
			
		||||
 | 
			
		||||
  date_created: Date
 | 
			
		||||
 | 
			
		||||
  date_done?: Date
 | 
			
		||||
 
 | 
			
		||||
@@ -32,11 +32,16 @@ export interface SystemStatus {
 | 
			
		||||
    redis_status: SystemStatusItemStatus
 | 
			
		||||
    redis_error: string
 | 
			
		||||
    celery_status: SystemStatusItemStatus
 | 
			
		||||
    celery_url: string
 | 
			
		||||
    celery_error: string
 | 
			
		||||
    index_status: SystemStatusItemStatus
 | 
			
		||||
    index_last_modified: string // ISO date string
 | 
			
		||||
    index_error: string
 | 
			
		||||
    classifier_status: SystemStatusItemStatus
 | 
			
		||||
    classifier_last_trained: string // ISO date string
 | 
			
		||||
    classifier_error: string
 | 
			
		||||
    sanity_check_status: SystemStatusItemStatus
 | 
			
		||||
    sanity_check_last_run: string // ISO date string
 | 
			
		||||
    sanity_check_error: string
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,11 @@ import {
 | 
			
		||||
} from '@angular/common/http/testing'
 | 
			
		||||
import { TestBed } from '@angular/core/testing'
 | 
			
		||||
import { environment } from 'src/environments/environment'
 | 
			
		||||
import { PaperlessTaskStatus, PaperlessTaskType } from '../data/paperless-task'
 | 
			
		||||
import {
 | 
			
		||||
  PaperlessTaskName,
 | 
			
		||||
  PaperlessTaskStatus,
 | 
			
		||||
  PaperlessTaskType,
 | 
			
		||||
} from '../data/paperless-task'
 | 
			
		||||
import { TasksService } from './tasks.service'
 | 
			
		||||
 | 
			
		||||
describe('TasksService', () => {
 | 
			
		||||
@@ -33,7 +37,7 @@ describe('TasksService', () => {
 | 
			
		||||
  it('calls tasks api endpoint on reload', () => {
 | 
			
		||||
    tasksService.reload()
 | 
			
		||||
    const req = httpTestingController.expectOne(
 | 
			
		||||
      `${environment.apiBaseUrl}tasks/`
 | 
			
		||||
      `${environment.apiBaseUrl}tasks/?task_name=consume_file&acknowledged=false`
 | 
			
		||||
    )
 | 
			
		||||
    expect(req.request.method).toEqual('GET')
 | 
			
		||||
  })
 | 
			
		||||
@@ -41,7 +45,9 @@ describe('TasksService', () => {
 | 
			
		||||
  it('does not call tasks api endpoint on reload if already loading', () => {
 | 
			
		||||
    tasksService.loading = true
 | 
			
		||||
    tasksService.reload()
 | 
			
		||||
    httpTestingController.expectNone(`${environment.apiBaseUrl}tasks/`)
 | 
			
		||||
    httpTestingController.expectNone(
 | 
			
		||||
      `${environment.apiBaseUrl}tasks/?task_name=consume_file&acknowledged=false`
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('calls acknowledge_tasks api endpoint on dismiss and reloads', () => {
 | 
			
		||||
@@ -55,14 +61,19 @@ describe('TasksService', () => {
 | 
			
		||||
    })
 | 
			
		||||
    req.flush([])
 | 
			
		||||
    // reload is then called
 | 
			
		||||
    httpTestingController.expectOne(`${environment.apiBaseUrl}tasks/`).flush([])
 | 
			
		||||
    httpTestingController
 | 
			
		||||
      .expectOne(
 | 
			
		||||
        `${environment.apiBaseUrl}tasks/?task_name=consume_file&acknowledged=false`
 | 
			
		||||
      )
 | 
			
		||||
      .flush([])
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('sorts tasks returned from api', () => {
 | 
			
		||||
    expect(tasksService.total).toEqual(0)
 | 
			
		||||
    const mockTasks = [
 | 
			
		||||
      {
 | 
			
		||||
        type: PaperlessTaskType.File,
 | 
			
		||||
        type: PaperlessTaskType.Auto,
 | 
			
		||||
        task_name: PaperlessTaskName.ConsumeFile,
 | 
			
		||||
        status: PaperlessTaskStatus.Complete,
 | 
			
		||||
        acknowledged: false,
 | 
			
		||||
        task_id: '1234',
 | 
			
		||||
@@ -70,7 +81,8 @@ describe('TasksService', () => {
 | 
			
		||||
        date_created: new Date(),
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        type: PaperlessTaskType.File,
 | 
			
		||||
        type: PaperlessTaskType.Auto,
 | 
			
		||||
        task_name: PaperlessTaskName.ConsumeFile,
 | 
			
		||||
        status: PaperlessTaskStatus.Failed,
 | 
			
		||||
        acknowledged: false,
 | 
			
		||||
        task_id: '1235',
 | 
			
		||||
@@ -78,7 +90,8 @@ describe('TasksService', () => {
 | 
			
		||||
        date_created: new Date(),
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        type: PaperlessTaskType.File,
 | 
			
		||||
        type: PaperlessTaskType.Auto,
 | 
			
		||||
        task_name: PaperlessTaskName.ConsumeFile,
 | 
			
		||||
        status: PaperlessTaskStatus.Pending,
 | 
			
		||||
        acknowledged: false,
 | 
			
		||||
        task_id: '1236',
 | 
			
		||||
@@ -86,7 +99,8 @@ describe('TasksService', () => {
 | 
			
		||||
        date_created: new Date(),
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        type: PaperlessTaskType.File,
 | 
			
		||||
        type: PaperlessTaskType.Auto,
 | 
			
		||||
        task_name: PaperlessTaskName.ConsumeFile,
 | 
			
		||||
        status: PaperlessTaskStatus.Started,
 | 
			
		||||
        acknowledged: false,
 | 
			
		||||
        task_id: '1237',
 | 
			
		||||
@@ -94,7 +108,8 @@ describe('TasksService', () => {
 | 
			
		||||
        date_created: new Date(),
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        type: PaperlessTaskType.File,
 | 
			
		||||
        type: PaperlessTaskType.Auto,
 | 
			
		||||
        task_name: PaperlessTaskName.ConsumeFile,
 | 
			
		||||
        status: PaperlessTaskStatus.Complete,
 | 
			
		||||
        acknowledged: false,
 | 
			
		||||
        task_id: '1238',
 | 
			
		||||
@@ -106,7 +121,7 @@ describe('TasksService', () => {
 | 
			
		||||
    tasksService.reload()
 | 
			
		||||
 | 
			
		||||
    const req = httpTestingController.expectOne(
 | 
			
		||||
      `${environment.apiBaseUrl}tasks/`
 | 
			
		||||
      `${environment.apiBaseUrl}tasks/?task_name=consume_file&acknowledged=false`
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    req.flush(mockTasks)
 | 
			
		||||
@@ -117,4 +132,19 @@ describe('TasksService', () => {
 | 
			
		||||
    expect(tasksService.queuedFileTasks).toHaveLength(1)
 | 
			
		||||
    expect(tasksService.startedFileTasks).toHaveLength(1)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('supports running tasks', () => {
 | 
			
		||||
    tasksService.run(PaperlessTaskName.SanityCheck).subscribe((res) => {
 | 
			
		||||
      expect(res).toEqual({
 | 
			
		||||
        result: 'success',
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
    const req = httpTestingController.expectOne(
 | 
			
		||||
      `${environment.apiBaseUrl}tasks/run/`
 | 
			
		||||
    )
 | 
			
		||||
    expect(req.request.method).toEqual('POST')
 | 
			
		||||
    req.flush({
 | 
			
		||||
      result: 'success',
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
import { HttpClient } from '@angular/common/http'
 | 
			
		||||
import { Injectable } from '@angular/core'
 | 
			
		||||
import { Subject } from 'rxjs'
 | 
			
		||||
import { Observable, Subject } from 'rxjs'
 | 
			
		||||
import { first, takeUntil } from 'rxjs/operators'
 | 
			
		||||
import {
 | 
			
		||||
  PaperlessTask,
 | 
			
		||||
  PaperlessTaskName,
 | 
			
		||||
  PaperlessTaskStatus,
 | 
			
		||||
  PaperlessTaskType,
 | 
			
		||||
} from 'src/app/data/paperless-task'
 | 
			
		||||
import { environment } from 'src/environments/environment'
 | 
			
		||||
 | 
			
		||||
@@ -14,6 +14,7 @@ import { environment } from 'src/environments/environment'
 | 
			
		||||
})
 | 
			
		||||
export class TasksService {
 | 
			
		||||
  private baseUrl: string = environment.apiBaseUrl
 | 
			
		||||
  private endpoint: string = 'tasks'
 | 
			
		||||
 | 
			
		||||
  public loading: boolean
 | 
			
		||||
 | 
			
		||||
@@ -54,10 +55,14 @@ export class TasksService {
 | 
			
		||||
    this.loading = true
 | 
			
		||||
 | 
			
		||||
    this.http
 | 
			
		||||
      .get<PaperlessTask[]>(`${this.baseUrl}tasks/`)
 | 
			
		||||
      .get<PaperlessTask[]>(
 | 
			
		||||
        `${this.baseUrl}${this.endpoint}/?task_name=consume_file&acknowledged=false`
 | 
			
		||||
      )
 | 
			
		||||
      .pipe(takeUntil(this.unsubscribeNotifer), first())
 | 
			
		||||
      .subscribe((r) => {
 | 
			
		||||
        this.fileTasks = r.filter((t) => t.type == PaperlessTaskType.File) // they're all File tasks, for now
 | 
			
		||||
        this.fileTasks = r.filter(
 | 
			
		||||
          (t) => t.task_name == PaperlessTaskName.ConsumeFile
 | 
			
		||||
        )
 | 
			
		||||
        this.loading = false
 | 
			
		||||
      })
 | 
			
		||||
  }
 | 
			
		||||
@@ -76,4 +81,13 @@ export class TasksService {
 | 
			
		||||
  public cancelPending(): void {
 | 
			
		||||
    this.unsubscribeNotifer.next(true)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public run(taskName: PaperlessTaskName): Observable<any> {
 | 
			
		||||
    return this.http.post<any>(
 | 
			
		||||
      `${environment.apiBaseUrl}${this.endpoint}/run/`,
 | 
			
		||||
      {
 | 
			
		||||
        task_name: taskName,
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -107,6 +107,7 @@ import {
 | 
			
		||||
  personFillLock,
 | 
			
		||||
  personLock,
 | 
			
		||||
  personSquare,
 | 
			
		||||
  playFill,
 | 
			
		||||
  plus,
 | 
			
		||||
  plusCircle,
 | 
			
		||||
  questionCircle,
 | 
			
		||||
@@ -312,6 +313,7 @@ const icons = {
 | 
			
		||||
  personFillLock,
 | 
			
		||||
  personLock,
 | 
			
		||||
  personSquare,
 | 
			
		||||
  playFill,
 | 
			
		||||
  plus,
 | 
			
		||||
  plusCircle,
 | 
			
		||||
  questionCircle,
 | 
			
		||||
 
 | 
			
		||||
@@ -21,10 +21,12 @@
 | 
			
		||||
  --pngx-success-darken-10: hsl(152, 69%, 11%); // based on success #198754
 | 
			
		||||
  --pngx-bg-alt: #fff;
 | 
			
		||||
  --pngx-bg-darker: var(--bs-gray-100);
 | 
			
		||||
  --pngx-bg-alt2: var(--bs-gray-200);
 | 
			
		||||
  --pngx-bg-alt2: var(--bs-gray-200); // #e9ecef
 | 
			
		||||
  --pngx-bg-disabled: #f7f7f7;
 | 
			
		||||
  --pngx-focus-alpha: 0.3;
 | 
			
		||||
  --pngx-toast-max-width: 360px;
 | 
			
		||||
  --bs-info: var(--pngx-bg-alt2);
 | 
			
		||||
  --bs-info-rgb: 233, 236, 239;
 | 
			
		||||
  @media screen and (min-width: 1024px) {
 | 
			
		||||
    --pngx-toast-max-width: 450px;
 | 
			
		||||
  }
 | 
			
		||||
@@ -71,8 +73,15 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,<svg xmlns='htt
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@mixin dark-mode {
 | 
			
		||||
  --bs-body-color: #{$text-color-dark-bg};
 | 
			
		||||
  --pngx-body-color-accent: #{$text-color-dark-bg-accent};
 | 
			
		||||
  --pngx-bg-alt: #242529;
 | 
			
		||||
  --pngx-bg-alt2: #232323;
 | 
			
		||||
  --pngx-bg-darker: #101216;
 | 
			
		||||
  --pngx-bg-disabled: var(--pngx-bg-alt);
 | 
			
		||||
  --pngx-focus-alpha: 0.6;
 | 
			
		||||
  --pngx-primary-faded: var(--pngx-primary-darken-15);
 | 
			
		||||
  --pngx-primary-text-contrast: var(--bs-body-color);
 | 
			
		||||
  --bs-body-color: #{$text-color-dark-bg};
 | 
			
		||||
  --bs-secondary-color: #6c757d;
 | 
			
		||||
  --bs-danger: #b71631;
 | 
			
		||||
  --bs-danger-rgb: 183, 22, 49;
 | 
			
		||||
@@ -80,15 +89,10 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,<svg xmlns='htt
 | 
			
		||||
  --bs-body-bg-rgb: 22, 22, 24;
 | 
			
		||||
  --bs-light: #1c1c1f;
 | 
			
		||||
  --bs-light-rgb: 28, 28, 31;
 | 
			
		||||
  --bs-info: var(--pngx-bg-alt);
 | 
			
		||||
  --bs-info-rgb: 36, 36, 39;
 | 
			
		||||
  --bs-border-color: #47494f;
 | 
			
		||||
  --pngx-bg-alt2: #232323;
 | 
			
		||||
  --pngx-bg-darker: #101216;
 | 
			
		||||
  --bs-tertiary-bg: var(--pngx-bg-darker);
 | 
			
		||||
  --pngx-bg-alt: #242529;
 | 
			
		||||
  --pngx-bg-disabled: var(--pngx-bg-alt);
 | 
			
		||||
  --pngx-focus-alpha: 0.6;
 | 
			
		||||
  --pngx-primary-faded: var(--pngx-primary-darken-15);
 | 
			
		||||
  --pngx-primary-text-contrast: var(--bs-body-color);
 | 
			
		||||
  --bs-dark-border-subtle: var(--pngx-bg-darker);
 | 
			
		||||
  --bs-border-color-translucent: rgba(0, 0, 0, .175); // override bs
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user