diff --git a/.gitignore b/.gitignore
index 86d943a9b2e8f3bb69fbe37fd8363962646b1d92..a35c24362d324a3fbd63f04b5d4dd8da9d26e651 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@
 /dist
 /tmp
 /out-tsc
+package-lock.json
 # Only exists if Bazel was run
 /bazel-out
 
diff --git a/deploy.sh b/deploy.sh
index e68fbcede16b7ca2310f747022a9f38160358f95..36eb4ca1ca2bcc5397ec2acb7c660f46202e2155 100755
--- a/deploy.sh
+++ b/deploy.sh
@@ -22,7 +22,6 @@ case $DESTINATION in
 esac
 
 ng build --optimization=true --prod=true
-ssh-add ~/.ssh/ssa16
 ssh $USER@$SERVER "rm -R /home/${SERVER}/content/vlass-manager/*"
 scp -r dist/vlass-manager/* $USER@$SERVER:/home/$SERVER/content/vlass-manager
 ssh $USER@$SERVER "chmod -R 775 /home/${SERVER}/content/vlass-manager/*"
diff --git a/src/app/app.component.html b/src/app/app.component.html
index 73030732527ddafbac9d723773d994969df87204..bcc916a91c68fdf865f0ce3fb1de88e53769af44 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -22,10 +22,10 @@
           <fa-icon [icon]="faSlidersH"></fa-icon>
           Settings
         </a>
-        <a class="col-auto" (click)="signIn()" href="javascript: void(0);" *ngIf="!isLoggedIn()">
+        <!--<a class="col-auto" (click)="signIn()" href="javascript: void(0);" *ngIf="!isLoggedIn()">
           <fa-icon [icon]="faSignInAlt"></fa-icon>
           Sign In
-        </a>
+        </a>-->
         <a class="col-auto" (click)="signOut()" href="javascript: void(0);" *ngIf="isLoggedIn()">
           <fa-icon [icon]="faSignOutAlt"></fa-icon>
           Sign Out
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index b3b4bb95abb09b721a781d3a3df978fa7f2a1e14..10e419e529002abb5a6258b27fd4129c1fc6ede8 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -18,6 +18,8 @@ import {
   faToggleOn
 } from "@fortawesome/free-solid-svg-icons";
 import {DOCUMENT} from "@angular/common";
+import {WINDOW} from "./env/window.provider";
+import {Title} from "@angular/platform-browser";
 
 @Component({
   selector: 'app-root',
@@ -42,13 +44,35 @@ export class AppComponent {
   public darkMode = false;
 
   constructor(
+    private titleService: Title,
     private authService: AuthService,
     private storageService: StorageService,
     private router: Router,
     private route: ActivatedRoute,
     private alertService: AlertService,
-    @Inject(DOCUMENT) document
+    @Inject(DOCUMENT) document,
+    @Inject(WINDOW) private window: Window
   ) {
+    // set the title based on the host
+    let title = 'VLASS Manager'
+    switch (this.window.location.hostname) {
+      case 'archive-test.nrao.edu':
+        title = 'TEST | ' + title;
+        break;
+      case 'archive-new.nrao.edu':
+        // leave the title alone
+        break;
+      case 'webtest.aoc.nrao.edu':
+        title = 'DEV | ' + title;
+        break;
+      case 'localhost':
+        title = 'LOCAL | ' + title;
+        break;
+      default:
+        title = '~?~ | ' + title
+        break;
+    }
+    this.titleService.setTitle(title);
     /*
     this.route.queryParams.subscribe(params => {
       if (params.hasOwnProperty('ticket')) {
@@ -85,9 +109,27 @@ export class AppComponent {
   }
 
   signOut(): void {
+    // delete all the cookies
+      const cookies = document.cookie.split("; ");
+      for (let c = 0; c < cookies.length; c++) {
+        const d = window.location.hostname.split(".");
+        while (d.length > 0) {
+          const cookieBase = encodeURIComponent(cookies[c].split(";")[0].split("=")[0]) + '=; expires=Thu, 01-Jan-1970 00:00:01 GMT; domain=' + d.join('.') + ' ;path=';
+          const p = location.pathname.split('/');
+          document.cookie = cookieBase + '/';
+          while (p.length > 0) {
+            document.cookie = cookieBase + p.join('/');
+            p.pop();
+          }
+          d.shift();
+        }
+      }
+      window.location.replace('https://my.nrao.edu/cas/logout?url=' + window.location.href);
+    /*
     this.alertService.success('You are logged out.');
     this.storageService.deleteLocal('jwt');
     this.router.navigate(['/']);
+     */
   }
 
   setDarkMode(dm: boolean): void {
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 43114c275fde6615ddaab051dd0f04d62db984d8..6c7b2e238bc33f88d0fb706558db10b7d2ea18f5 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -1,4 +1,4 @@
-import {BrowserModule} from '@angular/platform-browser';
+import {BrowserModule, Title} from '@angular/platform-browser';
 import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
 import {APP_INITIALIZER, NgModule} from '@angular/core';
 import {NgbModule} from "@ng-bootstrap/ng-bootstrap";
@@ -36,6 +36,7 @@ import {ExecutionDetailComponent} from './executions/execution/execution-detail/
 import {FontAwesomeModule} from "@fortawesome/angular-fontawesome";
 import {LoadingComponent} from './loading/loading.component';
 import {QueueSettingsComponent} from './settings/queue-settings/queue-settings.component';
+import {WINDOW_PROVIDERS} from "./env/window.provider";
 
 /*
 We will 'provide' this function below to load and set the global configuration from the
@@ -91,7 +92,7 @@ export function init_app(configService: ConfigurationService) {
     useFactory: init_app,
     deps: [ConfigurationService],
     multi: true
-  }],
+  }, Title, WINDOW_PROVIDERS],
   bootstrap: [AppComponent]
 })
 export class AppModule {
diff --git a/src/app/custom-http-param-encoder.spec.ts b/src/app/custom-http-param-encoder.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ff712d2c6044906904fce32439754f9bec28fcbf
--- /dev/null
+++ b/src/app/custom-http-param-encoder.spec.ts
@@ -0,0 +1,7 @@
+import {CustomHttpParamEncoder} from './custom-http-param-encoder';
+
+describe('CustomHttpParamEncoder', () => {
+  it('should create an instance', () => {
+    expect(new CustomHttpParamEncoder()).toBeTruthy();
+  });
+});
diff --git a/src/app/custom-http-param-encoder.ts b/src/app/custom-http-param-encoder.ts
new file mode 100644
index 0000000000000000000000000000000000000000..acf9597fb62d52ff8acc09a3fa7d560d1cb3914a
--- /dev/null
+++ b/src/app/custom-http-param-encoder.ts
@@ -0,0 +1,16 @@
+import {HttpParameterCodec} from '@angular/common/http';
+
+export class CustomHttpParamEncoder implements HttpParameterCodec {
+  encodeKey(key: string): string {
+    return encodeURIComponent(key);
+  }
+  encodeValue(value: string): string {
+    return encodeURIComponent(value);
+  }
+  decodeKey(key: string): string {
+    return decodeURIComponent(key);
+  }
+  decodeValue(value: string): string {
+    return decodeURIComponent(value);
+  }
+}
diff --git a/src/app/env/window.provider.ts b/src/app/env/window.provider.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bb7ba0938ce09b1d015a3a74b34b9d20a0ad1193
--- /dev/null
+++ b/src/app/env/window.provider.ts
@@ -0,0 +1,12 @@
+import {FactoryProvider, InjectionToken} from '@angular/core';
+
+export const WINDOW = new InjectionToken<Window>('window');
+
+const windowProvider: FactoryProvider = {
+  provide: WINDOW,
+  useFactory: () => window
+};
+
+export const WINDOW_PROVIDERS = [
+  windowProvider
+]
diff --git a/src/app/executions/execution/execution-detail/execution-detail.component.ts b/src/app/executions/execution/execution-detail/execution-detail.component.ts
index 5e3ac1c08576660214e774f498807408e8c4f4d3..86c8d782c195713a7147c92ee44804a5662666a9 100644
--- a/src/app/executions/execution/execution-detail/execution-detail.component.ts
+++ b/src/app/executions/execution/execution-detail/execution-detail.component.ts
@@ -39,7 +39,7 @@ export class ExecutionDetailComponent implements OnInit, OnDestroy {
   }
 
   ngOnInit() {
-    this.jobDetail$ = this.jobService.getJob(this.job.job_id).subscribe((j: JobExecution) => {
+    this.jobDetail$ = this.jobService.getJobExecution(this.job.job_id).subscribe((j: JobExecution) => {
       if (j) {
         this.jobDetail = j;
         this.noteFormGroup.get('notes').setValue(j.notes);
diff --git a/src/app/executions/execution/execution.component.html b/src/app/executions/execution/execution.component.html
index 79c7a0b38bcb547c1169d8dadcf4cc50024879ac..ad3e96fc782ec5be608feb53382fd280a3180bb2 100644
--- a/src/app/executions/execution/execution.component.html
+++ b/src/app/executions/execution/execution.component.html
@@ -29,7 +29,7 @@
   <div class="col-2">
     <div ngbDropdown>
       <button class="btn btn-xs" [ngClass]="getJobStatusClass()" type="button" ngbDropdownToggle>
-        {{ job.jobspec_status }}
+        {{ job.job_status }}
       </button>
       <ul ngbDropdownMenu>
         <li ngbDropdownItem *ngFor="let ps of productStatuses">
@@ -39,10 +39,9 @@
     </div>
   </div>
   <div class="col-1">
-    <h6><span class="badge ml-2"
-              [ngClass]="job.job_arch_status === 'ARCHIVED' ? 'badge-dark' : 'badge-light border'">
-            {{ job.job_arch_status }}
-        </span></h6>
+    <h6>
+      <span class="badge ml-2 {{getArchiveStatusClass()}}">{{ job.job_arch_status }}</span>
+    </h6>
   </div>
 </div>
 
diff --git a/src/app/executions/execution/execution.component.ts b/src/app/executions/execution/execution.component.ts
index 25781a0d5165dd3cd13c5d00c1983363e85b0df5..c18de924947ae1e461a6e139c16f428318ead2f0 100644
--- a/src/app/executions/execution/execution.component.ts
+++ b/src/app/executions/execution/execution.component.ts
@@ -77,8 +77,8 @@ export class ExecutionComponent implements OnInit, OnChanges {
     return this.configService.config.weblogbaseurl;
   }
 
-  getJobStatusClass() {
-    switch (this.job.jobspec_status) {
+  getJobStatusClass(): string {
+    switch (this.job.job_status) {
       case 'ERROR':
         return 'btn-danger';
       case 'QA_REJECTED':
@@ -96,6 +96,17 @@ export class ExecutionComponent implements OnInit, OnChanges {
     }
   }
 
+  getArchiveStatusClass(): string {
+    switch (this.job.job_arch_status) {
+      case 'ARCHIVED':
+        return 'badge-dark';
+      case 'ARCHIVE-ERROR':
+        return 'badge-error';
+      default:
+        return 'badge-light border';
+    }
+  }
+
   showWarning(job: Job): boolean {
     if (job.jobspec_status === 'PROCESSING') {
       let start_date = new Date(job.job_starttime);
diff --git a/src/app/executions/executions.component.html b/src/app/executions/executions.component.html
index a119028cd37afedcd4c3d5a3bf55d49f3c72b09e..6df2b6cc3ab9975cd1341258c9cb3619ecdf6fe3 100644
--- a/src/app/executions/executions.component.html
+++ b/src/app/executions/executions.component.html
@@ -2,6 +2,16 @@
   <div class="col-auto page-form">
 
     <div class="form-row p-2 pb-0 align-items-center">
+      <div class="col-auto pl-3"><b>Epoch</b>:</div>
+      <div class="btn-group col-auto" ngbDropdown>
+        <button class="btn btn-light btn-sm" type="button" id="epoch-select" ngbDropdownToggle>
+          {{ filters['EPOCH'].name }}</button>
+        <ul id="epoch-select-list" ngbDropdownMenu>
+          <li ngbDropdownItem *ngFor="let e of epochs" class="p-0">
+            <button type="button" class="btn btn-link w-100 text-left" (click)="setEpoch(e)">{{e.name}}</button>
+          </li>
+        </ul>
+      </div>
       <div class="col-auto pl-3"><b>Queue</b>:</div>
       <div class="btn-group col-auto" ngbDropdown>
         <button class="btn btn-light btn-sm" type="button" ngbDropdownToggle>{{ filters['JOB_QUEUE'].label }}</button>
@@ -20,15 +30,72 @@
           </li>
         </ul>
       </div>
-      <form (ngSubmit)="getJobs()" class="form-inline col-auto pl-4" [formGroup]="formGroup">
+      <form class="form-inline col-auto pl-4" [formGroup]="formGroup">
         <div class="form-group">
           <input type="text" class="form-control" id="pattern" placeholder="Pattern" formControlName="pattern">
         </div>
       </form>
+
+      <div class="col-auto pl-3"><b>Sort By</b>:</div>
+      <div class="btn-group col-auto" ngbDropdown>
+        <button class="btn btn-light btn-sm" type="button" id="sort-select" ngbDropdownToggle>
+          {{ getPrettyName(filters['JOB_SORT']) }}</button>
+        <ul ngbDropdownMenu>
+          <li ngbDropdownItem *ngFor="let col of sortColumns">
+            <button type="button" class="btn btn-link w-100 p-0 text-left" (click)="setSortColumn(col)">{{getPrettyName(col)}}</button>
+          </li>
+        </ul>
+      </div>
+      <div class="btn-group col-auto" ngbDropdown>
+        <button class="btn btn-light btn-sm" type="button" id="sort-direction" ngbDropdownToggle>
+          {{ filters['SORT_DIRECTION'] }}</button>
+        <ul ngbDropdownMenu>
+          <li ngbDropdownItem *ngFor="let direction of sortDirections">
+            <button type="button" class="btn btn-link w-100 p-0 text-left" (click)="setSortDirection(direction)">{{direction}}</button>
+          </li>
+        </ul>
+      </div>
+    </div>
+  </div>
+
+  <div class="col text-nowrap">
+    <p class="text-danger text-right p-2 m-0">{{ ((currentPage - 1) * resultsPerPage) + 1 }}
+      - {{ currentPage * resultsPerPage < numResults ? currentPage * resultsPerPage : numResults}}
+      of {{ numResults }}</p>
+  </div>
+  <div class="col-auto">
+
+    <div class="form-group m-0">
+      <div class="btn-group">
+        <button id="to-first-page" class="btn btn-danger btn-sm" (click)="goToPage(1)">
+          <fa-icon [icon]="faFastBackward"></fa-icon>
+        </button>
+        <button id="back-one-page" class="btn btn-danger btn-sm" (click)="goToPage(currentPage-1)">
+          <fa-icon [icon]="faStepBackward"></fa-icon>
+        </button>
+        <div class="btn-group" ngbDropdown>
+          <button class="btn btn-danger btn-sm" type="button" id="page-select" ngbDropdownToggle>
+            Page {{currentPage}}</button>
+          <ul style="height: 200px; overflow-y: scroll;" ngbDropdownMenu>
+            <li ngbDropdownItem *ngFor="let p of getPages(); let i = index" class="p-0">
+              <button type="button" class="btn btn-link w-100 text-left" (click)="goToPage(i+1)">Page {{i + 1}}</button>
+            </li>
+          </ul>
+        </div>
+        <button id="forward-one-page" class="btn btn-danger btn-sm" (click)="goToPage(currentPage+1)">
+          <fa-icon [icon]="faStepForward"></fa-icon>
+        </button>
+        <button id="to-last-page" class="btn btn-danger btn-sm" (click)="goToPage(pages)">
+          <fa-icon [icon]="faFastForward"></fa-icon>
+        </button>
+      </div>
     </div>
   </div>
+</div>
+
+<div class="row pb-2">
   <div class="col"></div>
-  <form class="col-auto form-inline" [formGroup]="alertThresholdForm">
+  <form class="col-auto form-inline text-right" [formGroup]="alertThresholdForm">
     <span class="text-warning ml-4 mr-2"><fa-icon [icon]="faExclamationTriangle"></fa-icon></span>
     after <input type="number" min="1" class="form-control form-control-sm mx-2 w-25" formControlName="threshold"/> days
   </form>
@@ -44,7 +111,7 @@
   </div>
   <ng-container *ngFor="let job of jobs">
     <app-execution-job [job]="job" [queue]="filters['JOB_QUEUE']" [alertThresholdDays]="alertAfterDays"
-                       (reload)="getJobs()"></app-execution-job>
+                       (reload)="getJobs(currentPage)"></app-execution-job>
   </ng-container>
 
 </ng-container>
diff --git a/src/app/executions/executions.component.ts b/src/app/executions/executions.component.ts
index bd685fe1e0a5a7ed571664809b6878b0a10d7e5c..8bd7723d0b2edc60fdb4f1347984f3f1e29ea5a0 100644
--- a/src/app/executions/executions.component.ts
+++ b/src/app/executions/executions.component.ts
@@ -7,7 +7,14 @@ import {debounceTime, distinctUntilChanged, map} from "rxjs/operators";
 import {ActivatedRoute} from "@angular/router";
 import {AlertService} from "../services/alert.service";
 import {FiltersService} from "../services/filters.service";
-import {faExclamationTriangle} from "@fortawesome/free-solid-svg-icons";
+import {Epoch} from "../model/epoch";
+import {
+  faExclamationTriangle,
+  faFastBackward,
+  faFastForward,
+  faStepBackward,
+  faStepForward
+} from "@fortawesome/free-solid-svg-icons";
 
 @Component({
   selector: 'app-executions',
@@ -16,15 +23,28 @@ import {faExclamationTriangle} from "@fortawesome/free-solid-svg-icons";
 })
 export class ExecutionsComponent implements OnInit, OnDestroy {
 
+  public epochs: Array<Epoch>;
   public queues: Array<JobQueue>;
   public statuses: Array<string>;
   public jobs$: Subscription;
   public jobs: Array<Job>;
   public pattern: string = "";
 
+  public resultsPerPage: number = 100;
+  public currentPage: number = 1;
+  public numResults: number = 0;
+  public pages: number = 1;
+  private pages$: Subscription;
+
   private filters$: Subscription;
   public filters: object;
+  public sortColumns: Array<string>;
+  public sortDirections: Array<string>;
 
+  public faFastBackward = faFastBackward;
+  public faStepBackward = faStepBackward;
+  public faFastForward = faFastForward;
+  public faStepForward = faStepForward;
   public faExclamationTriangle = faExclamationTriangle;
   public alertAfterDays = 14;
 
@@ -38,8 +58,11 @@ export class ExecutionsComponent implements OnInit, OnDestroy {
     private alertService: AlertService,
     private filterService: FiltersService
   ) {
+    this.epochs = this.filterService.getFilter('EPOCH');
     this.queues = this.filterService.getFilter('JOB_QUEUE');
     this.statuses = this.filterService.getFilter('JOB_STATUS');
+    this.sortColumns = this.filterService.getFilter('JOB_SORT');
+    this.sortDirections = this.filterService.getFilter('SORT_DIRECTION');
   }
 
   ngOnInit() {
@@ -55,6 +78,18 @@ export class ExecutionsComponent implements OnInit, OnDestroy {
       if (params.hasOwnProperty('status')) {
         this.filterService.setCurrentSetting('JOB_STATUS', params.status);
       }
+      if (params.hasOwnProperty('queue')) {
+        const paramQueue = Job.getQueueFromName(params.queue);
+        if (paramQueue) {
+          this.filterService.setCurrentSetting('JOB_QUEUE', paramQueue);
+        }
+      }
+      if (params.hasOwnProperty('epoch')) {
+        const paramEpoch = Epoch.getEpochFromId(params.epoch);
+        if (paramEpoch) {
+          this.filterService.setCurrentSetting('EPOCHS', paramEpoch);
+        }
+      }
     });
 
     this.formGroup.get('pattern').valueChanges.pipe(
@@ -63,12 +98,12 @@ export class ExecutionsComponent implements OnInit, OnDestroy {
       map(results => results)
     ).subscribe((val: string) => {
       this.pattern = val;
-      this.getJobs();
+      this.getPageInfoAndJob();
     });
 
     this.filters$ = this.filterService.currentSettings$.subscribe((filters: object) => {
       this.filters = filters;
-      this.getJobs();
+      this.getPageInfoAndJob();
     });
 
     this.alertThresholdForm = new FormGroup({
@@ -84,38 +119,109 @@ export class ExecutionsComponent implements OnInit, OnDestroy {
     });
   }
 
-  setQueue(queue: JobQueue) {
+  setEpoch(epoch: Epoch) {
+    this.filterService.setCurrentSetting('EPOCH', epoch);
+  }
+
+  setQueue(queue: JobQueue): void {
     this.filterService.setCurrentSetting('JOB_QUEUE', queue);
   }
 
-  setStatus(status: string) {
+  setStatus(status: string): void {
     this.filterService.setCurrentSetting('JOB_STATUS', status);
   }
 
-  getJobs() {
+  setSortColumn(column: string): void {
+    this.filterService.setCurrentSetting('JOB_SORT', column);
+  }
+
+  setSortDirection(direction: string): void {
+    this.filterService.setCurrentSetting('SORT_DIRECTION', direction);
+  }
+
+  getPrettyName(name: string): string {
+    return FiltersService.prettyName(name);
+  }
+
+  goToPage(page: number): boolean {
+    if (page < 1) {
+      page = 1;
+    }
+    if (page > this.pages) {
+      page = this.pages;
+    }
+    if (this.currentPage == page) {
+      return false;
+    }
+    this.currentPage = page;
+    this.getJobs(page);
+  }
+
+  getPages(): Array<any> {
+    return new Array(this.pages);
+  }
+
+  getPageCount(): void {
+    const epoch = this.filterService.getCurrentSetting('EPOCH');
+    const queue = this.filterService.getCurrentSetting('JOB_QUEUE');
+    const status = this.filterService.getCurrentSetting('JOB_STATUS');
+    this.pages$ = this.jobService.getJobRecordCount(epoch.id, queue.name, this.pattern, status).subscribe((jobNumber: number) => {
+      this.numResults = jobNumber;
+      this.pages = Math.ceil(jobNumber / this.resultsPerPage);
+      if (this.currentPage > this.pages) {
+        this.currentPage = this.pages;
+      }
+      if (this.currentPage < 1) {
+        this.currentPage = 1;
+      }
+      this.getJobs(1);
+    });
+  }
+
+  getJobs(pageId: number): void {
+    // clear a previous call
     if (this.jobs$) {
       this.jobs$.unsubscribe();
     }
     this.jobs = null;
-    var possibleId = parseInt(this.pattern);
-    var id = "";
-    if (!isNaN(possibleId)) {
-      id = this.pattern;
-    }
+    const epoch = this.filterService.getCurrentSetting('EPOCH');
     const queue = this.filterService.getCurrentSetting('JOB_QUEUE');
     const status = this.filterService.getCurrentSetting('JOB_STATUS');
-    this.alertService.info('Getting ' + queue.label + ' Jobs');
-    this.jobs$ = this.jobService.getJobs(queue.name, id, this.pattern, status).subscribe((j: Array<Job>) => {
-      if (j && j.length > 0) {
-        this.alertService.success(queue.label + ' jobs retrieved');
-        this.jobs = j;
+    this.alertService.info('Getting ' + queue.label);
+    this.jobs$ = this.jobService.getJobPage(epoch.id, queue.name, pageId - 1, this.pattern, status).subscribe((jobs: Array<Job>) => {
+      if (jobs && jobs.length > 0) {
+        this.alertService.success(queue.label + ' loaded.');
+        this.jobs = jobs;
       } else {
         this.jobs = [];
-        this.alertService.error('No ' + queue.label + ' jobs found');
+        this.alertService.error('No ' + queue.label + ' found.')
       }
     });
   }
 
+  getJobById(id: number): void {
+    // clear a previous call
+    if (this.jobs$) {
+      this.jobs$.unsubscribe();
+    }
+    this.jobs$ = this.jobService.getJobById(id).subscribe((job: Job) => {
+      this.jobs = [job];
+      this.numResults = 1;
+      this.pages = 1;
+      this.currentPage = 1;
+    });
+  }
+
+  getPageInfoAndJob(): void {
+    let possible_id = parseInt(this.pattern);
+    if (isNaN(possible_id)) {
+      this.getPageCount();
+      this.getJobs(1);
+    } else {
+      this.getJobById(possible_id);
+    }
+  }
+
   ngOnDestroy(): void {
     if (this.jobs$) {
       this.jobs$.unsubscribe();
@@ -123,5 +229,8 @@ export class ExecutionsComponent implements OnInit, OnDestroy {
     if (this.filters$) {
       this.filters$.unsubscribe();
     }
+    if (this.pages$) {
+      this.pages$.unsubscribe();
+    }
   }
 }
diff --git a/src/app/fileeditor/fileeditor.component.ts b/src/app/fileeditor/fileeditor.component.ts
index 989df533cb5c2d26bfe019a19d46d2eb24ca1c2c..61592b5dbad1cdfafe6166fce693d4e18f36ec40 100644
--- a/src/app/fileeditor/fileeditor.component.ts
+++ b/src/app/fileeditor/fileeditor.component.ts
@@ -70,7 +70,6 @@ export class FileeditorComponent implements OnInit {
         observe: "response",
         responseType: "text"
       }).pipe(map(response => {
-        console.log('get file: ', response);
         this.formGroup.get('content').setValue(response.body);
         return response.body;
       }));
@@ -96,9 +95,10 @@ export class FileeditorComponent implements OnInit {
       this.http.put(this.configService.config.url + '/services/' + this.displayedUrl, this.formGroup.get('content').value, {
         observe: "response", responseType: "text", headers: headers
       }).subscribe(response => {
-        console.log('put file: ', response);
         this.feedback = 'Save Successful';
         return response.body;
+      }, error => {
+        this.feedback = 'Something went wrong - ' + error;
       });
 
     } else {
diff --git a/src/app/jobspecs/jobspec/jobspec-detail/jobspec-detail.component.html b/src/app/jobspecs/jobspec/jobspec-detail/jobspec-detail.component.html
index 958d703e6fc8b2e6db55dec03f6619615062f967..786705add43a776aab4baaed4d846f382e5d3682 100644
--- a/src/app/jobspecs/jobspec/jobspec-detail/jobspec-detail.component.html
+++ b/src/app/jobspecs/jobspec/jobspec-detail/jobspec-detail.component.html
@@ -10,12 +10,6 @@
   Files
 </h5>
 <div class="row m-0 py-2 mb-1 border-bottom align-items-center" *ngFor="let file of jobspec.files">
-  <div class="col">
-    <a target="_blank"
-       href="{{getConfigUrl()}}/services/job/specs/{{ jobspec.id }}/files/{{ file.contentType }}/{{ file.name}}">
-      {{file.name}}
-    </a>&nbsp;
-  </div>
   <div class="col-auto">
     <a *ngIf="['WAITING','ERROR'].includes(jobspec.status)" class="btn btn-xs btn-outline-primary"
        [routerLink]="['/fileeditor', 'specs', jobspec.id, file.contentType, file.name]">
@@ -23,6 +17,12 @@
       Edit
     </a>
   </div>
+  <div class="col">
+    <a target="_blank"
+       href="{{getConfigUrl()}}/services/job/specs/{{ jobspec.id }}/files/{{ file.contentType }}/{{ file.name}}">
+      {{file.name}}
+    </a>&nbsp;
+  </div>
 </div>
 
 <h5 class="mt-2">
diff --git a/src/app/jobspecs/jobspec/jobspec-detail/jobspec-execution/jobspec-execution.component.html b/src/app/jobspecs/jobspec/jobspec-detail/jobspec-execution/jobspec-execution.component.html
index 69dd094aaa1e9d1f00c33103acc1368aed14996f..8a8e8f5f407ae8fb4daa5dea772692cf625fefe1 100644
--- a/src/app/jobspecs/jobspec/jobspec-detail/jobspec-execution/jobspec-execution.component.html
+++ b/src/app/jobspecs/jobspec/jobspec-detail/jobspec-execution/jobspec-execution.component.html
@@ -1,6 +1,6 @@
 <div class="row m-0 py-2 mb-1 border-bottom align-items-center">
   <div class="col">
-    <a [routerLink]="['/executions']" [queryParams]="{pattern: execution.id, status: execution.status}">{{execution.name}}</a>
+    <a [routerLink]="['/executions']" [queryParams]="{pattern: execution.id, status: execution.status, queue: execution.queueName}">{{execution.name}}</a>
   </div>
   <div class="col-auto">
     <h6><span class="badge" [ngClass]="getJobStatusClass(execution.status)">{{execution.status}}</span></h6>
diff --git a/src/app/jobspecs/jobspec/jobspec-detail/jobspec-execution/jobspec-execution.component.ts b/src/app/jobspecs/jobspec/jobspec-detail/jobspec-execution/jobspec-execution.component.ts
index dca06b00893102a88f683bd3ea77aaccf2b7fd47..d5fa1431d3cfc1e60afa692126ca2ae750e535bb 100644
--- a/src/app/jobspecs/jobspec/jobspec-detail/jobspec-execution/jobspec-execution.component.ts
+++ b/src/app/jobspecs/jobspec/jobspec-detail/jobspec-execution/jobspec-execution.component.ts
@@ -38,7 +38,7 @@ export class JobspecExecutionComponent implements OnInit, OnDestroy {
 
   getJob(jobId: number): void {
     this.showDetails = true;
-    this.job$ = this.jobService.getJob(jobId).subscribe((j: JobExecution) => {
+    this.job$ = this.jobService.getJobExecution(jobId).subscribe((j: JobExecution) => {
       if (j) {
         this.job = j;
       } else {
diff --git a/src/app/jobspecs/jobspec/jobspec-detail/jobspec-input/jobspec-input.component.html b/src/app/jobspecs/jobspec/jobspec-detail/jobspec-input/jobspec-input.component.html
index 298cbd04811431153716210d876122c17044940a..b84abdadb7815edac99c52bb7618bdb66e007d5d 100644
--- a/src/app/jobspecs/jobspec/jobspec-detail/jobspec-input/jobspec-input.component.html
+++ b/src/app/jobspecs/jobspec/jobspec-detail/jobspec-input/jobspec-input.component.html
@@ -1,5 +1,5 @@
 <a [routerLink]="['/products']"
-   [queryParams]="{epoch: input.productEpoch, type: input.productType, pattern: input.productName}">
+   [queryParams]="{epoch: input.productEpoch, type: input.productType, pattern: input.productId}">
   {{input.productName}}
 </a>
 <button type="button" class="btn btn-xs btn-outline-primary border-0 ml-2" (click)="toggleDetails(input.productId)">
diff --git a/src/app/jobspecs/jobspec/jobspec-detail/jobspec-target/jobspec-target.component.html b/src/app/jobspecs/jobspec/jobspec-detail/jobspec-target/jobspec-target.component.html
index 87ef25192351a41b4078e51215165c0960d232f6..192963b75223c963d6a392dd9a9d01a5bdfa487c 100644
--- a/src/app/jobspecs/jobspec/jobspec-detail/jobspec-target/jobspec-target.component.html
+++ b/src/app/jobspecs/jobspec/jobspec-detail/jobspec-target/jobspec-target.component.html
@@ -2,7 +2,7 @@
   <div class="col">
     <span class="badge badge-light border mr-1">Creating</span>
     <a [routerLink]="['/products']"
-       [queryParams]="{epoch: target.productEpoch, type: target.productType, pattern: target.id}">
+       [queryParams]="{epoch: target.productEpoch, type: getProductNameTypeFromId(target.productType), pattern: target.productId}">
       {{target.productName}}
     </a>
     <button type="button" class="btn btn-xs btn-outline-primary border-0 ml-2"
diff --git a/src/app/jobspecs/jobspec/jobspec-detail/jobspec-target/jobspec-target.component.ts b/src/app/jobspecs/jobspec/jobspec-detail/jobspec-target/jobspec-target.component.ts
index 08e46d3721de372ce71cbe598b7b07d1ad71cddd..5b5c430989d0bfac6af9a0468903548c3c761e3b 100644
--- a/src/app/jobspecs/jobspec/jobspec-detail/jobspec-target/jobspec-target.component.ts
+++ b/src/app/jobspecs/jobspec/jobspec-detail/jobspec-target/jobspec-target.component.ts
@@ -47,6 +47,10 @@ export class JobspecTargetComponent implements OnInit, OnDestroy {
     });
   }
 
+  getProductNameTypeFromId(id: number): string {
+    return Product.getTypeNameFromId(id);
+  }
+
   ngOnDestroy(): void {
     if (this.product$) {
       this.product$.unsubscribe();
diff --git a/src/app/jobspecs/jobspec/jobspec.component.html b/src/app/jobspecs/jobspec/jobspec.component.html
index 94904acf455aa12886a9b004c0b60687d5f8c5c5..a7b8e0e19b38bf80cc24ad12d3007df3df4acc64 100644
--- a/src/app/jobspecs/jobspec/jobspec.component.html
+++ b/src/app/jobspecs/jobspec/jobspec.component.html
@@ -12,7 +12,7 @@
       <button class="btn btn-link p-0" (click)="toggleDetails()">{{ job.jobspec_name }}</button>
     </ng-container>
     <ng-template #notHomeTabName>
-      <a [routerLink]="['/jobs']" [queryParams]="{pattern: job.jobspec_name}">{{job.jobspec_name}}</a>
+      <a [routerLink]="['/jobs']" [queryParams]="{pattern: job.jobspec_id, queuq: jobspec.queueName}">{{job.jobspec_name}}</a>
       <button class="btn btn-xs btn-outline-primary border-0 ml-2" (click)="toggleDetails()">
         <fa-icon [icon]="faCaretDown" *ngIf="!detailsExposed"></fa-icon>
         <fa-icon [icon]="faCaretUp" *ngIf="detailsExposed"></fa-icon>
diff --git a/src/app/jobspecs/jobspecs.component.html b/src/app/jobspecs/jobspecs.component.html
index 5084396e885d9c55e35f7005c7a1eec2be44ff79..a20316cd9ddac7e6202d426f1ea14476472b7dc9 100644
--- a/src/app/jobspecs/jobspecs.component.html
+++ b/src/app/jobspecs/jobspecs.component.html
@@ -2,9 +2,19 @@
   <div class="col-auto page-form">
 
     <div class="form-row p-2 pb-0 align-items-center">
+      <div class="col-auto pl-3"><b>Epoch</b>:</div>
+      <div class="btn-group col-auto" ngbDropdown>
+        <button class="btn btn-light btn-sm" type="button" id="epoch-select" ngbDropdownToggle>
+          {{ filters['EPOCH'].name }}</button>
+        <ul id="epoch-select-list" ngbDropdownMenu>
+          <li ngbDropdownItem *ngFor="let e of epochs" class="p-0">
+            <button type="button" class="btn btn-link w-100 text-left" (click)="setEpoch(e)">{{e.name}}</button>
+          </li>
+        </ul>
+      </div>
       <div class="col-auto pl-3"><b>Queue</b>:</div>
       <div class="btn-group col-auto" ngbDropdown>
-        <button class="btn btn-light btn-sm" type="button" id="page-select" ngbDropdownToggle>
+        <button class="btn btn-light btn-sm" type="button" ngbDropdownToggle>
           {{ filters['JOB_QUEUE'].label }}</button>
         <ul ngbDropdownMenu>
           <li ngbDropdownItem *ngFor="let q of queues" class="p-0">
@@ -22,14 +32,67 @@
           </li>
         </ul>
       </div>
-      <form (ngSubmit)="getJobs()" class="form-inline col-auto pl-4" [formGroup]="formGroup">
+      <form class="form-inline col-auto pl-4" [formGroup]="formGroup">
         <div class="form-group">
           <input type="text" class="form-control" id="pattern" placeholder="Pattern" formControlName="pattern">
         </div>
       </form>
+
+      <div class="col-auto pl-3"><b>Sort By</b>:</div>
+      <div class="btn-group col-auto" ngbDropdown>
+        <button class="btn btn-light btn-sm" type="button" id="sort-select" ngbDropdownToggle>
+          {{ getPrettyName(filters['JOBSPEC_SORT']) }}</button>
+        <ul ngbDropdownMenu>
+          <li ngbDropdownItem *ngFor="let col of sortColumns">
+            <button type="button" class="btn btn-link w-100 p-0 text-left" (click)="setSortColumn(col)">{{getPrettyName(col)}}</button>
+          </li>
+        </ul>
+      </div>
+      <div class="btn-group col-auto" ngbDropdown>
+        <button class="btn btn-light btn-sm" type="button" id="sort-direction" ngbDropdownToggle>
+          {{ filters['SORT_DIRECTION'] }}</button>
+        <ul ngbDropdownMenu>
+          <li ngbDropdownItem *ngFor="let direction of sortDirections">
+            <button type="button" class="btn btn-link w-100 p-0 text-left" (click)="setSortDirection(direction)">{{direction}}</button>
+          </li>
+        </ul>
+      </div>
     </div>
   </div>
+
   <div class="col">
+    <p class="text-danger text-right p-2 m-0">{{ ((currentPage - 1) * resultsPerPage) + 1 }}
+      - {{ currentPage * resultsPerPage < numResults ? currentPage * resultsPerPage : numResults}}
+      of {{ numResults }}</p>
+  </div>
+  <div class="col-auto">
+
+    <div class="form-group m-0">
+      <div class="btn-group">
+        <button id="to-first-page" class="btn btn-danger btn-sm" (click)="goToPage(1)">
+          <fa-icon [icon]="faFastBackward"></fa-icon>
+        </button>
+        <button id="back-one-page" class="btn btn-danger btn-sm" (click)="goToPage(currentPage-1)">
+          <fa-icon [icon]="faStepBackward"></fa-icon>
+        </button>
+        <div class="btn-group" ngbDropdown>
+          <button class="btn btn-danger btn-sm" type="button" id="page-select" ngbDropdownToggle>
+            Page {{currentPage}}</button>
+          <ul style="height: 200px; overflow-y: scroll;" ngbDropdownMenu>
+            <li ngbDropdownItem *ngFor="let p of getPages(); let i = index" class="p-0">
+              <button type="button" class="btn btn-link w-100 text-left" (click)="goToPage(i+1)">Page {{i + 1}}</button>
+            </li>
+          </ul>
+        </div>
+        <button id="forward-one-page" class="btn btn-danger btn-sm" (click)="goToPage(currentPage+1)">
+          <fa-icon [icon]="faStepForward"></fa-icon>
+        </button>
+        <button id="to-last-page" class="btn btn-danger btn-sm" (click)="goToPage(pages)">
+          <fa-icon [icon]="faFastForward"></fa-icon>
+        </button>
+      </div>
+    </div>
+
   </div>
 </div>
 
@@ -45,7 +108,7 @@
     <div class="col-auto px-2 py-3 thead_th" *ngIf="canDeleteJobs">&nbsp;</div>
   </div>
   <ng-container *ngFor="let job of jobs">
-    <app-jobspec [job]="job" [canDeleteJob]="canDeleteJobs" (refreshJobs)="getJobs()"></app-jobspec>
+    <app-jobspec [job]="job" [canDeleteJob]="canDeleteJobs" (refreshJobs)="getPageInfoAndJobSpec()"></app-jobspec>
   </ng-container>
 
 </ng-container>
diff --git a/src/app/jobspecs/jobspecs.component.ts b/src/app/jobspecs/jobspecs.component.ts
index 78200007de08169c60e1765b175aa63e5b1b053d..a35424014148587114daed40a4736c3377ddd768 100644
--- a/src/app/jobspecs/jobspecs.component.ts
+++ b/src/app/jobspecs/jobspecs.component.ts
@@ -9,6 +9,8 @@ import {AlertService} from "../services/alert.service";
 import {FiltersService} from "../services/filters.service";
 import {Setting} from "../model/setting";
 import {SettingsService} from "../services/settings.service";
+import {Epoch} from "../model/epoch";
+import {faFastBackward, faFastForward, faStepBackward, faStepForward} from "@fortawesome/free-solid-svg-icons";
 
 @Component({
   selector: 'app-jobspecs',
@@ -17,20 +19,34 @@ import {SettingsService} from "../services/settings.service";
 })
 export class JobspecsComponent implements OnInit, OnDestroy {
 
+  public epochs: Array<Epoch>;
   public queues: Array<JobQueue>;
   public statuses: Array<string>;
   public jobs$: Subscription;
-  public jobs: Array<Job>;
+  public jobs: Array<JobSpec>;
   public pattern: string = "";
 
+  public resultsPerPage: number = 100;
+  public currentPage: number = 1;
+  public numResults: number = 0;
+  public pages: number = 1;
+  private pages$: Subscription;
+
   private readonly settings$: Subscription;
   public canDeleteJobs = false;
 
   private filters$: Subscription;
   public filters: object;
+  public sortColumns: Array<string>;
+  public sortDirections: Array<string>;
 
   public formGroup: FormGroup;
 
+  public faFastBackward = faFastBackward;
+  public faStepBackward = faStepBackward;
+  public faFastForward = faFastForward;
+  public faStepForward = faStepForward;
+
   constructor(
     private jobService: JobsService,
     private route: ActivatedRoute,
@@ -38,6 +54,7 @@ export class JobspecsComponent implements OnInit, OnDestroy {
     private filterService: FiltersService,
     private settingsService: SettingsService
   ) {
+    this.epochs = this.filterService.getFilter('EPOCH');
     this.queues = this.filterService.getFilter('JOB_QUEUE');
     this.statuses = this.filterService.getFilter('JOB_STATUS');
     this.settings$ = this.settingsService.getSettings().subscribe((s: Setting) => {
@@ -49,6 +66,8 @@ export class JobspecsComponent implements OnInit, OnDestroy {
     }, error => {
       this.canDeleteJobs = false;
     });
+    this.sortColumns = this.filterService.getFilter('JOBSPEC_SORT');
+    this.sortDirections = this.filterService.getFilter('SORT_DIRECTION');
   }
 
   ngOnInit() {
@@ -70,6 +89,12 @@ export class JobspecsComponent implements OnInit, OnDestroy {
           this.filterService.setCurrentSetting('JOB_QUEUE', paramQueue);
         }
       }
+      if (params.hasOwnProperty('epoch')) {
+        const paramEpoch = Epoch.getEpochFromId(params.epoch);
+        if (paramEpoch) {
+          this.filterService.setCurrentSetting('EPOCHS', paramEpoch);
+        }
+      }
     });
 
     this.formGroup.get('pattern').valueChanges.pipe(
@@ -78,13 +103,18 @@ export class JobspecsComponent implements OnInit, OnDestroy {
       map(results => results)
     ).subscribe((val: string) => {
       this.pattern = val;
-      this.getJobs();
+      this.getPageInfoAndJobSpec();
     });
 
     this.filters$ = this.filterService.currentSettings$.subscribe((filters: object) => {
       this.filters = filters;
-      this.getJobs();
+      this.getPageInfoAndJobSpec();
     });
+    this.getPageInfoAndJobSpec();
+  }
+
+  setEpoch(epoch: Epoch) {
+    this.filterService.setCurrentSetting('EPOCH', epoch);
   }
 
   setQueue(queue: JobQueue): void {
@@ -95,6 +125,99 @@ export class JobspecsComponent implements OnInit, OnDestroy {
     this.filterService.setCurrentSetting('JOB_STATUS', status);
   }
 
+  setSortColumn(column: string): void {
+    this.filterService.setCurrentSetting('JOBSPEC_SORT', column);
+  }
+
+  setSortDirection(direction: string): void {
+    this.filterService.setCurrentSetting('SORT_DIRECTION', direction);
+  }
+
+  getPrettyName(name: string): string {
+    return FiltersService.prettyName(name);
+  }
+
+  goToPage(page: number): boolean {
+    if (page < 1) {
+      page = 1;
+    }
+    if (page > this.pages) {
+      page = this.pages;
+    }
+    if (this.currentPage == page) {
+      return false;
+    }
+    this.currentPage = page;
+    this.getJobSpecs();
+  }
+
+  getPages(): Array<any> {
+    return new Array(this.pages);
+  }
+
+  getPageCount(): void {
+    const epoch = this.filterService.getCurrentSetting('EPOCH');
+    const queue = this.filterService.getCurrentSetting('JOB_QUEUE');
+    const status = this.filterService.getCurrentSetting('JOB_STATUS');
+    this.pages$ = this.jobService.getJobSpecRecordCount(epoch.id, queue.name, this.pattern, status).subscribe((jobSpecNumber: number) => {
+      this.numResults = jobSpecNumber;
+      this.pages = Math.ceil(jobSpecNumber / this.resultsPerPage);
+      console.log('num', jobSpecNumber);
+      if (this.currentPage > this.pages) {
+        this.currentPage = this.pages;
+      }
+      if (this.currentPage < 1) {
+        this.currentPage = 1;
+      }
+      this.getJobSpecs();
+    });
+  }
+
+  getJobSpecs(): void {
+    // clear a previous call
+    if (this.jobs$) {
+      this.jobs$.unsubscribe();
+    }
+    this.jobs = null;
+    const epoch = this.filterService.getCurrentSetting('EPOCH');
+    const queue = this.filterService.getCurrentSetting('JOB_QUEUE');
+    const status = this.filterService.getCurrentSetting('JOB_STATUS');
+    this.alertService.info('Getting ' + queue.label);
+    this.jobs$ = this.jobService.getJobSpecPage(epoch.id, queue.name, this.currentPage - 1, this.pattern, status).subscribe((jobSpecs: Array<JobSpec>) => {
+      if (jobSpecs && jobSpecs.length > 0) {
+        this.alertService.success(queue.label + ' loaded.');
+        this.jobs = jobSpecs;
+      } else {
+        this.jobs = [];
+        this.alertService.error('No ' + queue.label + ' found.')
+      }
+    });
+  }
+
+  getJobSpecById(id: number): void {
+    // clear a previous call
+    if (this.jobs$) {
+      this.jobs$.unsubscribe();
+    }
+    this.jobs$ = this.jobService.getJobSpecById(id).subscribe((jobSpec: JobSpec) => {
+      this.jobs = [jobSpec];
+      this.numResults = 1;
+      this.pages = 1;
+      this.currentPage = 1;
+    });
+  }
+
+  getPageInfoAndJobSpec(): void {
+    let possible_id = parseInt(this.pattern);
+    if (isNaN(possible_id)) {
+      this.getPageCount();
+      this.getJobSpecs();
+    } else {
+      this.getJobSpecById(possible_id);
+    }
+  }
+
+  /*
   getJobs(): void {
     if (this.jobs$) {
       this.jobs$.unsubscribe();
@@ -127,6 +250,7 @@ export class JobspecsComponent implements OnInit, OnDestroy {
       });
     }
   }
+   */
 
   ngOnDestroy(): void {
     if (this.jobs$) {
@@ -138,6 +262,9 @@ export class JobspecsComponent implements OnInit, OnDestroy {
     if (this.settings$) {
       this.settings$.unsubscribe();
     }
+    if (this.pages$) {
+      this.pages$.unsubscribe();
+    }
   }
 
 }
diff --git a/src/app/model/job.ts b/src/app/model/job.ts
index 2a49d13e46b7d96829d725a01a0760093a603146..e869e996cf6a655e3bd4c0e1ab5a95208f8d2fb2 100644
--- a/src/app/model/job.ts
+++ b/src/app/model/job.ts
@@ -41,6 +41,8 @@ export class Job {
       }
     }
   }
+
+  static SORT_COLUMNS = ['id', 'name', 'start_date', 'end_date', 'status'];
 }
 
 export class Task {
@@ -94,4 +96,6 @@ export class JobSpec {
   sdmId: string;
   status: string;
   targets: Array<ProductVersion>;
+
+  static SORT_COLUMNS = ['id', 'name', 'creation_date', 'status'];
 }
diff --git a/src/app/model/product.ts b/src/app/model/product.ts
index 465a5880830da3d3cf35851df11dccc1c8fdb163..7c33ab3ade83dbcadbc9272b4b69868755552043 100644
--- a/src/app/model/product.ts
+++ b/src/app/model/product.ts
@@ -93,6 +93,7 @@ export class Product {
   ];
 
   static getTypeFromName(typeName: string): ProductType {
+    typeName = typeName === 'rawdata' ? 'schedblock' : typeName;
     for (const type of this.TYPES) {
       if (type.name === typeName) {
         return type;
@@ -100,6 +101,14 @@ export class Product {
     }
   }
 
+  static getTypeNameFromId(typeId: number): string {
+    for (const type of this.TYPES) {
+      if (type.id === typeId) {
+        return type.name;
+      }
+    }
+  }
+
   static getIdFromTypeName(typeName: string): number {
     for (const type of this.TYPES) {
       if (type.name === typeName) {
@@ -108,4 +117,6 @@ export class Product {
     }
   }
 
+  static SORT_COLUMNS = ['id', 'name', 'status'];
+
 }
diff --git a/src/app/model/tile.ts b/src/app/model/tile.ts
index 91b11aa84271ed8ed3e8664dad377e6076e2195a..38401199cf68f7da12ad14a14d49d57ee367b03e 100644
--- a/src/app/model/tile.ts
+++ b/src/app/model/tile.ts
@@ -16,6 +16,8 @@ export class Tile {
   raMin: number;
   tier: number;
   scans: Array<object>;
+
+  static SORT_COLUMNS = ['id', 'name', 'status'];
 }
 
 export class PhaseCenterDeg {
diff --git a/src/app/products/product/product-details/product-details.component.html b/src/app/products/product/product-details/product-details.component.html
index dee3fc266cc01837dc696aa7229b6b8df76cc308..98c88dfa811fac3eea06139b96951a1fbc5a22b8 100644
--- a/src/app/products/product/product-details/product-details.component.html
+++ b/src/app/products/product/product-details/product-details.component.html
@@ -49,26 +49,41 @@
 </div>
 
 <div *ngIf="typeConfig | async">
-  <a class="btn btn-sm btn-outline-info bg-light float-right" [routerLink]="['/fileeditor','type', type.id, 'json','configurations']">
-    <fa-icon [icon]="faEdit"></fa-icon>
-  </a>
+
   <h5 class="my-3">
     <fa-icon [icon]="faFileCode"></fa-icon>
     Type Parameters
   </h5>
-  <div class="bg-secondary text-light p-2 my-2">{{ (typeConfig | async) | json}}</div>
+  <div class="form-row my-2">
+    <div class="col-auto">
+      <a class="btn btn-sm btn-outline-info bg-light"
+         [routerLink]="['/fileeditor','type', type.id, 'json','configurations']">
+        <fa-icon [icon]="faEdit"></fa-icon>
+      </a>
+    </div>
+    <div class="col">
+      <div class="bg-secondary text-light p-2">{{ (typeConfig | async) | json}}</div>
+    </div>
+  </div>
+
 </div>
 
 <div *ngIf="productConfig | async">
-  <a class="btn btn-sm btn-outline-info bg-light float-right"
-     [routerLink]="['/fileeditor','product', product.id, 'json','configurations']">
-    <fa-icon [icon]="faEdit"></fa-icon>
-  </a>
   <h5 class="my-3">
     <fa-icon [icon]="faFileCode"></fa-icon>
     Product Parameters
   </h5>
-  <div class="bg-secondary text-light p-2 my-2">{{ (productConfig | async) | json}}</div>
+  <div class="form-row my-2">
+    <div class="col-auto">
+      <a class="btn btn-sm btn-outline-info bg-light"
+         [routerLink]="['/fileeditor','product', product.id, 'json','configurations']">
+        <fa-icon [icon]="faEdit"></fa-icon>
+      </a>
+    </div>
+    <div class="col">
+      <div class="bg-secondary text-light p-2">{{ (productConfig | async) | json}}</div>
+    </div>
+  </div>
 </div>
 
 <div *ngIf="mergedConfig | async">
diff --git a/src/app/products/product/product-details/product-prerequsites/product-prerequsites.component.html b/src/app/products/product/product-details/product-prerequsites/product-prerequsites.component.html
index 05768072ed8da58b21c4001a961fa259174b1247..7371222f0e05f78602588f525ab04c54fe39f530 100644
--- a/src/app/products/product/product-details/product-prerequsites/product-prerequsites.component.html
+++ b/src/app/products/product/product-details/product-prerequsites/product-prerequsites.component.html
@@ -3,7 +3,7 @@
     <span class="badge badge-light border">{{pre.name}}</span>
   </div>
   <div class="col">
-    <a [routerLink]="['/products']" [queryParams]="{pattern: pre.requiredProduct.id}">
+    <a [routerLink]="['/products']" [queryParams]="{pattern: pre.requiredProduct.id, type: pre.name}">
       {{pre.requiredProduct.name }}
     </a>
     <button type="button" class="btn btn-xs btn-outline-primary border-0 ml-2" (click)="showDetails = !showDetails">
diff --git a/src/app/products/product/product-details/product-version/product-version.component.html b/src/app/products/product/product-details/product-version/product-version.component.html
index f67bf97c9704757ec01b0525f9060a00aa2b89a9..cff86d4c990b9505f31f49d2da8421653fca691f 100644
--- a/src/app/products/product/product-details/product-version/product-version.component.html
+++ b/src/app/products/product/product-details/product-version/product-version.component.html
@@ -5,7 +5,7 @@
   <div class="col">
     <ng-container *ngFor="let js of (version.jobSpecifications | keyvalue)">
       <span class="badge badge-light border mr-1">Job</span>
-      <a [routerLink]="['/jobs']" [queryParams]="{pattern: js.key}">{{js.value.name}}</a>
+      <a [routerLink]="['/jobs']" [queryParams]="{pattern: js.key, type: js.value.queue}">{{js.value.name}}</a>
       <button type="button" class="btn btn-xs btn-outline-primary border-0 ml-2"
               (click)="toggleDetails(js.key.toString())">
         <fa-icon [icon]="faCaretDown" *ngIf="!showDetails"></fa-icon>
diff --git a/src/app/products/products.component.html b/src/app/products/products.component.html
index 21c321e719c9804eae3476df4ef8c21706ce0679..f175a9802ab52994f0389d9ff40d1621738d3d72 100644
--- a/src/app/products/products.component.html
+++ b/src/app/products/products.component.html
@@ -23,17 +23,38 @@
         </ul>
       </div>
 
-      <form class="form-inline col pl-4" [formGroup]="formGroup">
+      <form class="form-inline col-auto pl-4" [formGroup]="formGroup">
         <div class="form-group">
           <input type="text" class="form-control" id="pattern" placeholder="Pattern/Id" formControlName="pattern">
         </div>
       </form>
+
+      <div class="col-auto pl-3"><b>Sort By</b>:</div>
+      <div class="btn-group col-auto" ngbDropdown>
+        <button class="btn btn-light btn-sm" type="button" id="sort-select" ngbDropdownToggle>
+          {{ getPrettyName(filters['PRODUCT_SORT']) }}</button>
+        <ul ngbDropdownMenu>
+          <li ngbDropdownItem *ngFor="let col of sortColumns">
+            <button type="button" class="btn btn-link w-100 p-0 text-left" (click)="setSortColumn(col)">{{getPrettyName(col)}}</button>
+          </li>
+        </ul>
+      </div>
+      <div class="btn-group col-auto" ngbDropdown>
+        <button class="btn btn-light btn-sm" type="button" id="sort-direction" ngbDropdownToggle>
+          {{ filters['SORT_DIRECTION'] }}</button>
+        <ul ngbDropdownMenu>
+          <li ngbDropdownItem *ngFor="let direction of sortDirections">
+            <button type="button" class="btn btn-link w-100 p-0 text-left" (click)="setSortDirection(direction)">{{direction}}</button>
+          </li>
+        </ul>
+      </div>
     </div>
   </div>
 
   <div class="col">
     <p class="text-danger text-right p-2 m-0">{{ ((currentPage - 1) * resultsPerPage) + 1 }}
-      - {{ currentPage * resultsPerPage}} of {{ numResults }}</p>
+      - {{ currentPage * resultsPerPage < numResults ? currentPage * resultsPerPage : numResults}}
+      of {{ numResults }}</p>
   </div>
   <div class="col-auto">
 
@@ -103,6 +124,7 @@
             <label class="control-label" for="sbid">Enter OPT SB ID:</label>
             <input type="number" class="form-control" name="sbid" id="sbid" placeholder="36158915"
                    formControlName="sbid"/>
+            <div class="text-danger" *ngIf="!productsbId.pristine && !productsbId.valid">SB ID can only be digits</div>
           </div>
         </div>
         <div class="col-xs-12 col-md-6">
@@ -127,12 +149,14 @@
   <div class="row no-gutters mx-1">
     <div class="col-1 px-2 py-3 thead_th">Id</div>
     <div class="col px-2 py-3 thead_th">Name</div>
-    <div class="col px-2 py-3 thead_th" *ngIf="showImageDetails">Phase Center (deg) <span class="ml-3">Image Size (deg)</span></div>
+    <div class="col px-2 py-3 thead_th" *ngIf="showImageDetails">Phase Center (deg) <span
+      class="ml-3">Image Size (deg)</span></div>
     <div class="col-auto px-2 py-3 thead_th">Status</div>
     <div class="col-auto px-2 py-3 thead_th" *ngIf="canDeleteProducts">&nbsp;</div>
   </div>
   <ng-container *ngFor="let product of products">
-    <app-product [product]="product" [canDeleteProducts]="canDeleteProducts" (refreshProducts)="getProducts(currentPage)"></app-product>
+    <app-product [product]="product" [canDeleteProducts]="canDeleteProducts"
+                 (refreshProducts)="getPageInfoAndProduct()"></app-product>
   </ng-container>
 
 </ng-container>
diff --git a/src/app/products/products.component.ts b/src/app/products/products.component.ts
index f9b3146d9704d0136db80b7969f74221ab3672b2..005ade615501f0e67858fbc1b35488b1b403c62c 100644
--- a/src/app/products/products.component.ts
+++ b/src/app/products/products.component.ts
@@ -34,6 +34,8 @@ export class ProductsComponent implements OnInit, OnDestroy {
 
   private filters$: Subscription;
   public filters: any;
+  public sortColumns: Array<string>;
+  public sortDirections: Array<string>;
 
   private readonly settings$: Subscription;
   public canDeleteProducts = false;
@@ -81,13 +83,15 @@ export class ProductsComponent implements OnInit, OnDestroy {
     }, error => {
       this.canDeleteProducts = false;
     });
+    this.sortColumns = this.filterService.getFilter('PRODUCT_SORT');
+    this.sortDirections = this.filterService.getFilter('SORT_DIRECTION');
   }
 
   ngOnInit() {
     this.productFormGroup = new FormGroup({
       epoch: new FormControl(null, Validators.required),
       sbname: new FormControl(null, {validators: Validators.required, updateOn: "blur"}),
-      sbid: new FormControl(null, [Validators.required, Validators.pattern('^[0-9]+$')]),
+      sbid: new FormControl(null, [Validators.required, Validators.min(1), Validators.max(999999999)]),
       minitiles: new FormControl(null, Validators.required)
     });
 
@@ -109,7 +113,7 @@ export class ProductsComponent implements OnInit, OnDestroy {
       if (params.hasOwnProperty('type')) {
         const paramType = Product.getTypeFromName(params.type);
         if (paramType) {
-          this.filterService.setCurrentSetting('PRODUCT_TYPES', paramType);
+          this.filterService.setCurrentSetting('PRODUCT_TYPE', paramType);
         }
       }
       if (params.hasOwnProperty('epoch')) {
@@ -162,6 +166,18 @@ export class ProductsComponent implements OnInit, OnDestroy {
     this.filterService.setCurrentSetting('PRODUCT_STATUS', status);
   }
 
+  setSortColumn(column: string): void {
+    this.filterService.setCurrentSetting('PRODUCT_SORT', column);
+  }
+
+  setSortDirection(direction: string): void {
+    this.filterService.setCurrentSetting('SORT_DIRECTION', direction);
+  }
+
+  getPrettyName(name: string): string {
+    return FiltersService.prettyName(name);
+  }
+
   goToPage(page: number): boolean {
     if (page < 1) {
       page = 1;
@@ -173,7 +189,7 @@ export class ProductsComponent implements OnInit, OnDestroy {
       return false;
     }
     this.currentPage = page;
-    this.getProducts(page);
+    this.getProducts();
   }
 
   getPages(): Array<any> {
@@ -186,12 +202,17 @@ export class ProductsComponent implements OnInit, OnDestroy {
     this.pages$ = this.productService.getRecordCount(epoch.id, type.id, this.pattern).subscribe((productNumber: number) => {
       this.numResults = productNumber;
       this.pages = Math.ceil(productNumber / this.resultsPerPage);
-      this.currentPage = 1;
-      this.getProducts(1);
+      if (this.currentPage > this.pages) {
+        this.currentPage = this.pages;
+      }
+      if (this.currentPage < 1) {
+        this.currentPage = 1;
+      }
+      this.getProducts();
     });
   }
 
-  getProducts(pageId: number): void {
+  getProducts(): void {
     // clear a previous call
     if (this.products$) {
       this.products$.unsubscribe();
@@ -200,7 +221,7 @@ export class ProductsComponent implements OnInit, OnDestroy {
     const epoch = this.filterService.getCurrentSetting('EPOCH');
     const type = this.filterService.getCurrentSetting('PRODUCT_TYPE');
     this.alertService.info('Getting ' + type.label);
-    this.products$ = this.productService.getPage(epoch.id, type.id, pageId - 1, this.pattern).subscribe((products: Array<Product>) => {
+    this.products$ = this.productService.getPage(epoch.id, type.id, this.currentPage - 1, this.pattern).subscribe((products: Array<Product>) => {
       if (products && products.length > 0) {
         this.alertService.success(type.label + ' loaded.');
         this.products = products;
@@ -228,7 +249,7 @@ export class ProductsComponent implements OnInit, OnDestroy {
     let possible_id = parseInt(this.pattern);
     if (isNaN(possible_id)) {
       this.getPageCount();
-      this.getProducts(1);
+      this.getProducts();
     } else {
       this.getProductById(possible_id.toString());
     }
@@ -247,17 +268,38 @@ export class ProductsComponent implements OnInit, OnDestroy {
 
   generateProduct() {
     this.alertService.info('Generating products...');
-    this.schedblockService.createSchedblock(
-      this.productFormGroup.get('epoch').value,
-      this.productFormGroup.get('sbname').value,
-      this.productFormGroup.get('sbid').value,
-      this.productFormGroup.get('minitiles').value
-    ).subscribe(response => {
-      this.alertService.success('Products Generated');
-      this.getProducts(this.currentPage);
-    }, (error) => {
-      this.alertService.error('Product generating failed');
-    });
+    if (this.productFormGroup.valid) {
+      this.schedblockService.createSchedblock(
+        this.productEpoch.value,
+        this.productSBname.value,
+        this.productsbId.value,
+        this.productMinitiles.value
+      ).subscribe(response => {
+        this.alertService.success('Products Generated');
+        this.getProducts();
+      }, (error) => {
+        this.alertService.error('Product generating failed');
+      });
+    } else {
+      this.alertService.error('Invalid values in the form');
+      console.log(this.productFormGroup.value);
+    }
+  }
+
+  /**
+   * conveinence getters for the form
+   */
+  get productEpoch(): FormControl {
+    return this.productFormGroup.get('epoch') as FormControl;
+  }
+  get productSBname(): FormControl {
+    return this.productFormGroup.get('sbname') as FormControl;
+  }
+  get productsbId(): FormControl {
+    return this.productFormGroup.get('sbid') as FormControl;
+  }
+  get productMinitiles(): FormControl {
+    return this.productFormGroup.get('minitiles') as FormControl;
   }
 
   ngOnDestroy(): void {
diff --git a/src/app/services/filters.service.ts b/src/app/services/filters.service.ts
index 4d29071f3c3aa6d9711b57539350833cd0fbd9a0..ff08a17b278f87c3c2fa54d866279fb9149b3b16 100644
--- a/src/app/services/filters.service.ts
+++ b/src/app/services/filters.service.ts
@@ -1,9 +1,10 @@
 import {Injectable} from '@angular/core';
 import {Product} from "../model/product";
-import {Job} from "../model/job";
+import {Job, JobSpec} from "../model/job";
 import {BehaviorSubject, Observable} from "rxjs";
 import {StorageService} from "./storage.service";
 import {Epoch} from "../model/epoch";
+import {Tile} from "../model/tile";
 
 
 @Injectable({
@@ -16,7 +17,12 @@ export class FiltersService {
     'PRODUCT_TYPE': Product.TYPES,
     'PRODUCT_STATUS': ['ALL', 'COMPLETED', 'WAITING', 'READY', 'COMPLETED', 'PROCESSING', 'CACHED'],
     'JOB_QUEUE': Job.QUEUES,
-    'JOB_STATUS': ['ALL', 'WAITING', 'PROCESSING', 'QA_READY', 'QA_MANUAL', 'QA_ACCEPTED', 'QA_REJECTED', 'QA_ON_HOLD', 'QA_MANUALLY_ACCEPTED', 'ERROR']
+    'JOB_STATUS': ['ALL', 'WAITING', 'PROCESSING', 'QA_READY', 'QA_MANUAL', 'QA_ACCEPTED', 'QA_REJECTED', 'QA_ON_HOLD', 'QA_MANUAL_ACCEPTED', 'ERROR'],
+    'SORT_DIRECTION': ['ASC', 'DESC'],
+    'TILE_SORT': Tile.SORT_COLUMNS,
+    'PRODUCT_SORT': Product.SORT_COLUMNS,
+    'JOBSPEC_SORT': JobSpec.SORT_COLUMNS,
+    'JOB_SORT': Job.SORT_COLUMNS
   };
 
   private defaultSettings = {
@@ -24,7 +30,12 @@ export class FiltersService {
     'PRODUCT_TYPE': Product.getTypeFromName('calibration'),
     'PRODUCT_STATUS': 'ALL',
     'JOB_QUEUE': Job.getQueueFromName('calibration'),
-    'JOB_STATUS': 'ALL'
+    'JOB_STATUS': 'ALL',
+    'SORT_DIRECTION': 'ASC',
+    'TILE_SORT': 'id',
+    'PRODUCT_SORT': 'id',
+    'JOBSPEC_SORT': 'id',
+    'JOB_SORT': 'id'
   };
 
   private currentSettings = {};
@@ -55,6 +66,15 @@ export class FiltersService {
     this._currentSettings.next(this.currentSettings);
   }
 
+  static prettyName(name: string): string {
+    if (name.length < 3) {
+      return name.toUpperCase();
+    } else {
+      name = name.replace(/_/g, ' ');
+      return name.toLowerCase().split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
+    }
+  }
+
   getFilters() {
     return this.filters
   }
diff --git a/src/app/services/jobs.service.ts b/src/app/services/jobs.service.ts
index 83588857ff62f64bc3bed1c2928d361a76b153c0..97bf2c47c3acf027b9b8d80e174044b0d5a4297a 100644
--- a/src/app/services/jobs.service.ts
+++ b/src/app/services/jobs.service.ts
@@ -1,9 +1,11 @@
 import {Injectable} from '@angular/core';
-import {HttpClient, HttpResponse} from "@angular/common/http";
+import {HttpClient, HttpParams} from "@angular/common/http";
 import {ConfigurationService} from "../env/configuration.service";
 import {Observable} from "rxjs";
 import {map, switchMap} from "rxjs/operators";
 import {Job, JobExecution, JobSpec} from "../model/job";
+import {FiltersService} from "./filters.service";
+import {CustomHttpParamEncoder} from "../custom-http-param-encoder";
 
 @Injectable({
   providedIn: 'root'
@@ -12,42 +14,69 @@ export class JobsService {
 
   endPoint: string = '/services/job/';
 
-  constructor(private http: HttpClient, private configService: ConfigurationService) {
-  }
-
-  public getJobs(queue: string, id: string, pattern: string, status: string): Observable<Array<Job>> {
-    return this.http.get<Array<Job>>(this.configService.config.url + this.endPoint + 'optimized/queues/' + queue, {observe: 'response'}).pipe(
-      map((response: HttpResponse<Array<Job>>) => {
-        let reply = response.body;
-        if (id && id.length > 0) {
-          reply = reply.filter(job => job.job_id.toString() === id);
-        } else {
-          if (pattern && pattern.length > 0) {
-            reply = reply.filter(job => job.job_name.match(pattern.replace("+", "\\+")));
-          }
-          if (status && status.length > 0 && status !== 'ALL') {
-            reply = reply.filter(job => job.job_status == status);
-          }
-        }
-        return reply;
+  constructor(
+    private http: HttpClient,
+    private configService: ConfigurationService,
+    private filtersService: FiltersService) {
+  }
+
+  public getJobRecordCount(epoch: number, queue: string, pattern: string, status: string): Observable<number> {
+    return this.http.get<number>(this.configService.config.url + this.endPoint + 'byEpoch/' + epoch.toString() + '/byQueue/' + queue +
+      '/pages?pattern=' + encodeURIComponent(pattern) + '&status=' + encodeURI(status), {observe: 'response'}).pipe(
+      map(response => {
+        return response.body;
       }));
   }
 
-  public getJobSpecs(queue: string, id: string, pattern: string, status: string): Observable<Array<Job>> {
-    return this.http.get<Array<Job>>(this.configService.config.url + this.endPoint + 'optimized/queues/' + queue, {observe: 'response'}).pipe(
-      map((response: HttpResponse<Array<Job>>) => {
-        let reply = response.body;
-        if (id && id.length > 0) {
-          reply = reply.filter(job => job.jobspec_id.toString() === id);
-        } else {
-          if (pattern && pattern.length > 0) {
-            reply = reply.filter(job => job.jobspec_name.match(pattern.replace("+", "\\+")));
-          }
-          if (status && status.length > 0 && status !== 'ALL') {
-            reply = reply.filter(job => job.job_status == status);
-          }
-        }
-        return reply;
+  public getJobSpecRecordCount(epoch: number, queue: string, pattern: string, status: string): Observable<number> {
+    return this.http.get<number>(this.configService.config.url + this.endPoint + 'specs/byEpoch/' + epoch.toString() + '/byQueue/' + queue +
+      '/pages?pattern=' + encodeURIComponent(pattern) + '&status=' + encodeURI(status), {observe: 'response'}).pipe(
+      map(response => {
+        return response.body;
+      }));
+  }
+
+  public getJobSpecPage(epoch: number, queue: string, pageId: number, pattern: string, status: string): Observable<Array<JobSpec>> {
+    let params = new HttpParams({encoder: new CustomHttpParamEncoder()});
+    if (!!pattern) {
+      params = params.append('pattern', pattern);
+    }
+    if (!!status) {
+      params = params.append('status', status);
+    }
+
+    const sortName = this.filtersService.getCurrentSetting('JOBSPEC_SORT');
+    const sortDir = this.filtersService.getCurrentSetting('SORT_DIRECTION');
+
+    params = params.append('columnNames', sortName);
+    params = params.append('directions', sortDir);
+
+    return this.http.get<Array<JobSpec>>(this.configService.config.url + this.endPoint + 'specs/byEpoch/' + epoch.toString() + '/byQueue/' + queue +
+      '/pages/' + pageId.toString() + '/', {params: params, observe: 'response'}).pipe(
+      map(response => {
+        return response.body;
+      }));
+  }
+
+  public getJobPage(epoch: number, queue: string, pageId: number, pattern: string, status: string): Observable<Array<Job>> {
+    let params = new HttpParams({encoder: new CustomHttpParamEncoder()});
+    if (!!pattern) {
+      params = params.append('pattern', pattern);
+    }
+    if (!!status) {
+      params = params.append('status', status);
+    }
+
+    const sortName = this.filtersService.getCurrentSetting('JOB_SORT');
+    const sortDir = this.filtersService.getCurrentSetting('SORT_DIRECTION');
+
+    params = params.append('columnNames', sortName);
+    params = params.append('directions', sortDir);
+
+    return this.http.get<Array<Job>>(this.configService.config.url + this.endPoint + 'byEpoch/' + epoch.toString() + '/byQueue/' + queue +
+      '/pages/' + pageId.toString() + '/', {params: params, observe: 'response'}).pipe(
+      map(response => {
+        return response.body;
       }));
   }
 
@@ -58,6 +87,14 @@ export class JobsService {
       }));
   }
 
+  // unlike the above, this returns an object the same shape as the pages
+  public getJobSpecById(id: number): Observable<JobSpec> {
+    return this.http.get<JobSpec>(this.configService.config.url + this.endPoint + 'specs/byId/' + id, {observe: 'response'}).pipe(
+      map(response => {
+        return response.body;
+      }));
+  }
+
   public jobSpecToJob(spec: JobSpec): Job {
     const summary = new Job();
     summary.jobspec_id = spec.id;
@@ -80,13 +117,22 @@ export class JobsService {
     return summary;
   }
 
-  public getJob(id: number): Observable<any> {
+
+  public getJobExecution(id: number): Observable<JobExecution> {
     return this.http.get<JobExecution>(this.configService.config.url + this.endPoint + 'jobs/' + id, {observe: 'response'}).pipe(
       map(response => {
         return response.body;
       }));
   }
 
+  // unlike the above, this returns an object the same shape as the pages
+  public getJobById(id: number): Observable<Job> {
+    return this.http.get<Job>(this.configService.config.url + this.endPoint + 'byId/' + id, {observe: 'response'}).pipe(
+      map(response => {
+        return response.body;
+      }));
+  }
+
   public createJob(id: number, queue: string, version: number): Observable<any> {
     let command = {productId: id, inputProductVersions: [version]};
     return this.http.post(this.configService.config.url + this.endPoint + 'createJob?id=' + id + '&queue=' + queue, command, {observe: "response"}).pipe(
diff --git a/src/app/services/products.service.ts b/src/app/services/products.service.ts
index a327813763f5c56d84c44a88afdbff737b8df7f6..8e0a3c8134c368c3451ac0a1a6f040a4c0feb973 100644
--- a/src/app/services/products.service.ts
+++ b/src/app/services/products.service.ts
@@ -4,6 +4,8 @@ import {ConfigurationService} from "../env/configuration.service";
 import {Observable} from "rxjs";
 import {map} from "rxjs/operators";
 import {Product} from "../model/product";
+import {FiltersService} from "./filters.service";
+import {CustomHttpParamEncoder} from "../custom-http-param-encoder";
 
 @Injectable({
   providedIn: 'root'
@@ -12,22 +14,32 @@ export class ProductsService {
 
   endPoint: string = '/services/product/';
 
-  constructor(private http: HttpClient, private configService: ConfigurationService) {
+  constructor(
+    private http: HttpClient,
+    private configService: ConfigurationService,
+    private filtersService: FiltersService) {
   }
 
   public getRecordCount(epoch: number, type: number, pattern: string): Observable<number> {
     return this.http.get<number>(this.configService.config.url + this.endPoint + 'byEpoch/' + epoch.toString() + '/byType/' + type.toString() +
-      '/pages?pattern=' + encodeURI(pattern), {observe: 'response'}).pipe(
+      '/pages?pattern=' + encodeURIComponent(pattern), {observe: 'response'}).pipe(
       map(response => {
         return response.body;
       }));
   }
 
   public getPage(epoch: number, type: number, pageId: number, pattern: string): Observable<Array<Product>> {
-    let params = new HttpParams();
+    let params = new HttpParams({encoder: new CustomHttpParamEncoder()});
     if (pattern) {
       params = params.append('pattern', pattern);
     }
+
+    const sortName = this.filtersService.getCurrentSetting('PRODUCT_SORT');
+    const sortDir = this.filtersService.getCurrentSetting('SORT_DIRECTION');
+
+    params = params.append('columnNames', sortName);
+    params = params.append('directions', sortDir);
+
     return this.http.get<Array<Product>>(this.configService.config.url + this.endPoint + 'byEpoch/' + epoch.toString() + '/byType/' + type.toString() +
       '/pages/' + pageId.toString() + '/', {params: params, observe: 'response'}).pipe(
       map(response => {
diff --git a/src/app/services/tiles.service.ts b/src/app/services/tiles.service.ts
index d8a123290a9d5f783fb1548bda5814aaf95977d9..fdede641e3b80120b8314ebcccde8a4d6df63a1a 100644
--- a/src/app/services/tiles.service.ts
+++ b/src/app/services/tiles.service.ts
@@ -4,6 +4,7 @@ import {Observable} from "rxjs";
 import {ConfigurationService} from "../env/configuration.service";
 import {map} from "rxjs/operators";
 import {Tile, TileDefinition} from "../model/tile";
+import {FiltersService} from "./filters.service";
 
 @Injectable({
   providedIn: 'root'
@@ -12,13 +13,21 @@ export class TilesService {
 
   endPoint: string = '/services/minitiles/';
 
-  constructor(private configService: ConfigurationService, private http: HttpClient) {
+  constructor(
+    private configService: ConfigurationService,
+    private http: HttpClient,
+    private filterService: FiltersService) {
   }
 
   public getTilesForEpoch(pattern: string, epoch: number): Observable<Array<Tile>> {
     let params = new HttpParams();
 
+    const sortName = this.filterService.getCurrentSetting('TILE_SORT');
+    const sortDir = this.filterService.getCurrentSetting('SORT_DIRECTION');
+
     params = params.append('pattern', pattern);
+    params = params.append('columnNames', sortName);
+    params = params.append('directions', sortDir);
 
     return this.http.get<Array<Tile>>(this.configService.config.url + this.endPoint + 'summary/epoch/' + epoch.toString(), {
       observe: 'response',
@@ -28,6 +37,13 @@ export class TilesService {
     }));
   }
 
+  public updateTileSummary(epoch: number): Observable<any> {
+    return this.http.put<any>(this.configService.config.url + this.endPoint + 'summary/update?epoch=' + epoch.toString(), {}, {observe: 'response'})
+      .pipe(map(response => {
+      return response.body;
+    }));
+  }
+
   public getTileDefinition(id: number): Observable<TileDefinition> {
     return this.http.get<TileDefinition>(this.configService.config.url + this.endPoint + id + '/definitions',
       {observe: 'response'}).pipe(map(response => {
diff --git a/src/app/tiles/tiles.component.html b/src/app/tiles/tiles.component.html
index 57e6ec197c693ed73b4a56972cbcb024136b66a1..8e5c2c8898baf17e03e4077b3f180794b0f33a13 100644
--- a/src/app/tiles/tiles.component.html
+++ b/src/app/tiles/tiles.component.html
@@ -8,17 +8,17 @@
           {{ getEpochName(epoch) }}</button>
         <ul ngbDropdownMenu>
           <li ngbDropdownItem>
-            <button type="button" class="btn btn-link p-0" (click)="setEpoch(-1)">Tests</button>
+            <button type="button" class="btn btn-link w-100 p-0 text-left" (click)="setEpoch(-1)">Tests</button>
           </li>
           <li ngbDropdownItem>
-            <button type="button" class="btn btn-link p-0" (click)="setEpoch(0)">Pilot</button>
+            <button type="button" class="btn btn-link w-100 p-0 text-left" (click)="setEpoch(0)">Pilot</button>
           </li>
           <li ngbDropdownItem>
-            <button type="button" class="btn btn-link p-0" (click)="setEpoch(1)">Epoch 1</button>
+            <button type="button" class="btn btn-link w-100 p-0 text-left" (click)="setEpoch(1)">Epoch 1</button>
           </li>
           <li ngbDropdownItem>
-          <button type="button" class="btn btn-link p-0" (click)="setEpoch(2)">Epoch 2</button>
-        </li>
+            <button type="button" class="btn btn-link w-100 p-0 text-left" (click)="setEpoch(2)">Epoch 2</button>
+          </li>
         </ul>
       </div>
       <form (ngSubmit)="getMinitilesForEpoch()" class="form-inline col-auto pl-4" [formGroup]="formGroup">
@@ -26,13 +26,33 @@
           <input type="text" class="form-control" id="pattern" placeholder="Pattern" formControlName="pattern">
         </div>
       </form>
+
+      <div class="col-auto pl-3"><b>Sort By</b>:</div>
+      <div class="btn-group col" ngbDropdown>
+        <button class="btn btn-light btn-sm" type="button" id="sort-select" ngbDropdownToggle>
+          {{ getPrettyName(filters['TILE_SORT']) }}</button>
+        <ul ngbDropdownMenu>
+          <li ngbDropdownItem *ngFor="let col of sortColumns">
+            <button type="button" class="btn btn-link w-100 p-0 text-left" (click)="setSortColumn(col)">{{getPrettyName(col)}}</button>
+          </li>
+        </ul>
+      </div>
+      <div class="btn-group col" ngbDropdown>
+        <button class="btn btn-light btn-sm" type="button" id="sort-direction" ngbDropdownToggle>
+          {{ filters['SORT_DIRECTION'] }}</button>
+        <ul ngbDropdownMenu>
+          <li ngbDropdownItem *ngFor="let direction of sortDirections">
+            <button type="button" class="btn btn-link w-100 p-0 text-left" (click)="setSortDirection(direction)">{{direction}}</button>
+          </li>
+        </ul>
+      </div>
     </div>
   </div>
   <div class="col"></div>
   <div class="col-auto text-right">
-    <button type="button" class="btn btn-danger btn-sm" (click)="getMinitilesForEpoch()">
+    <button type="button" class="btn btn-danger btn-sm" (click)="updateSummary()">
       <fa-icon [icon]="faSyncAlt"></fa-icon>
-      Refresh
+      Update
     </button>
   </div>
 </div>
diff --git a/src/app/tiles/tiles.component.ts b/src/app/tiles/tiles.component.ts
index efde0d218d062bca0a9a82e1a56917edfef2a602..8c8fd76399bcc3252e48c1f72a0a7f5a86b34b38 100644
--- a/src/app/tiles/tiles.component.ts
+++ b/src/app/tiles/tiles.component.ts
@@ -6,6 +6,7 @@ import {Tile} from "../model/tile";
 import {FormControl, FormGroup} from "@angular/forms";
 import {AlertService} from "../services/alert.service";
 import {faSyncAlt} from "@fortawesome/free-solid-svg-icons";
+import {FiltersService} from "../services/filters.service";
 
 @Component({
   selector: 'app-tiles',
@@ -23,7 +24,23 @@ export class TilesComponent implements OnInit, OnDestroy {
 
   public faSyncAlt = faSyncAlt;
 
-  constructor(private tileService: TilesService, private alertService: AlertService) {
+  private readonly filters$: Subscription;
+  public filters: any;
+  public sortColumns: Array<string>;
+  public sortDirections: Array<string>;
+
+
+  constructor(
+    private tileService: TilesService,
+    private alertService: AlertService,
+    private filtersService: FiltersService) {
+    this.sortColumns = this.filtersService.getFilter('TILE_SORT');
+    this.sortDirections = this.filtersService.getFilter('SORT_DIRECTION');
+
+    this.filters$ = this.filtersService.currentSettings$.subscribe((filters: object) => {
+      this.filters = filters;
+      this.getMinitilesForEpoch();
+    });
   }
 
   ngOnInit() {
@@ -48,6 +65,18 @@ export class TilesComponent implements OnInit, OnDestroy {
     this.getMinitilesForEpoch();
   }
 
+  setSortColumn(column: string): void {
+    this.filtersService.setCurrentSetting('TILE_SORT', column);
+  }
+
+  setSortDirection(direction: string): void {
+    this.filtersService.setCurrentSetting('SORT_DIRECTION', direction);
+  }
+
+  getPrettyName(name: string): string {
+    return FiltersService.prettyName(name);
+  }
+
   getEpochName(epoch: number): string {
     switch (epoch) {
       case -1:
@@ -59,9 +88,18 @@ export class TilesComponent implements OnInit, OnDestroy {
     }
   }
 
+  updateSummary(): void {
+    this.alertService.info('Updating tile summary');
+    this.tileService.updateTileSummary(this.epoch).subscribe(() => {
+      this.getMinitilesForEpoch();
+    }, error => {
+      this.alertService.error('Tile summary update failed');
+    });
+  }
+
   getMinitilesForEpoch() {
-    this.tiles = null;
     this.alertService.info('Getting Tiles');
+    this.tiles = null;
     this.tiles$ = this.tileService.getTilesForEpoch(this.pattern, this.epoch).subscribe((mt: Array<Tile>) => {
       if (mt && mt.length > 0) {
         this.alertService.success('Tiles retrieved');
@@ -70,12 +108,16 @@ export class TilesComponent implements OnInit, OnDestroy {
         this.alertService.error('Tiles could not be retrieved');
       }
     });
+
   }
 
   ngOnDestroy(): void {
     if (this.tiles$) {
       this.tiles$.unsubscribe();
     }
+    if (this.filters$) {
+      this.filters$.unsubscribe();
+    }
   }
 
 }
diff --git a/src/env.js b/src/env.js
index 0c7cf5ac0306b82a506282640b0ae838d88ee6ab..0e01b750538fe2172f68303cd328737c780a5183 100644
--- a/src/env.js
+++ b/src/env.js
@@ -13,6 +13,9 @@ A service in angular will capture this info and add it to a service
     case 'archive-new.nrao.edu':
       window.__env.configUrl = 'https://archive-new.nrao.edu/VlassMngr/services/configuration';
       break;
+    case 'localhost':
+      window.__env.configUrl = 'http://localhost:8080/VlassMngr/services/configuration';
+      break;
     default:
       window.__env.configUrl = 'https://webtest.aoc.nrao.edu/VlassMngr/services/configuration';
       break;
diff --git a/src/index.html b/src/index.html
index 9cf72a44c6eb93d532ae6af14690034da956c6a1..f5a4e74f659dfac66a9ef81e30dc9a2bb2bd371c 100644
--- a/src/index.html
+++ b/src/index.html
@@ -2,7 +2,7 @@
 <html lang="en">
 <head>
   <meta charset="utf-8">
-  <title>VlassMngr2</title>
+  <title>VLASS Manager</title>
   <base href="./">
 
   <meta name="viewport" content="width=device-width, initial-scale=1">