|
@@ -1,9 +1,12 @@
|
|
|
-import { Component, OnInit } from '@angular/core';
|
|
|
|
|
|
|
+import { Component, inject } from '@angular/core';
|
|
|
import { CommonModule } from '@angular/common';
|
|
import { CommonModule } from '@angular/common';
|
|
|
|
|
+import { Store } from '@ngxs/store';
|
|
|
|
|
+import { firstValueFrom } from 'rxjs';
|
|
|
|
|
+import { BaseComponent } from 'angularlib/base.component';
|
|
|
|
|
+import { DpService } from 'dp-ui/dp.service';
|
|
|
|
|
+import { FisAppMessage } from 'dp-ui/fisappmessage/apprequestmessagetype';
|
|
|
import { LocalHistoryService } from '../../services/local-history.service';
|
|
import { LocalHistoryService } from '../../services/local-history.service';
|
|
|
-import { RemoteInferenceService } from '../../core/services/remote-inference.service';
|
|
|
|
|
-import { VisionSocketService } from '../../services/vision-socket.service';
|
|
|
|
|
-import { environment } from '../../../environments/environment';
|
|
|
|
|
|
|
+import { NgxSocketService } from 'dp-ui/socket/ngxSocket.service';
|
|
|
|
|
|
|
|
const GRADE_COLORS: Record<string, string> = {
|
|
const GRADE_COLORS: Record<string, string> = {
|
|
|
'Empty_Bunch': '#6C757D', 'Underripe': '#F9A825', 'Abnormal': '#DC3545',
|
|
'Empty_Bunch': '#6C757D', 'Underripe': '#F9A825', 'Abnormal': '#DC3545',
|
|
@@ -12,7 +15,7 @@ const GRADE_COLORS: Record<string, string> = {
|
|
|
|
|
|
|
|
export interface BatchSessionGroup {
|
|
export interface BatchSessionGroup {
|
|
|
batch_id: string;
|
|
batch_id: string;
|
|
|
- label: string; // e.g. "Batch Session · April 20"
|
|
|
|
|
|
|
+ label: string;
|
|
|
count: number;
|
|
count: number;
|
|
|
timestamp: string;
|
|
timestamp: string;
|
|
|
records: any[];
|
|
records: any[];
|
|
@@ -26,54 +29,61 @@ export interface BatchSessionGroup {
|
|
|
templateUrl: './history.component.html',
|
|
templateUrl: './history.component.html',
|
|
|
styleUrls: ['./history.component.scss']
|
|
styleUrls: ['./history.component.scss']
|
|
|
})
|
|
})
|
|
|
-export class HistoryComponent implements OnInit {
|
|
|
|
|
|
|
+export class HistoryComponent extends BaseComponent {
|
|
|
|
|
+ private readonly dpService = inject(DpService);
|
|
|
|
|
+ private readonly localHistory = inject(LocalHistoryService);
|
|
|
|
|
+ private readonly _ngxSocket = inject(NgxSocketService);
|
|
|
|
|
+ readonly visionSocket = { nestStatus: () => this._ngxSocket.status === 'online' ? 'ONLINE' as const : 'OFFLINE' as const };
|
|
|
|
|
+
|
|
|
localHistoryRecords: any[] = [];
|
|
localHistoryRecords: any[] = [];
|
|
|
remoteHistoryRecords: any[] = [];
|
|
remoteHistoryRecords: any[] = [];
|
|
|
- /** Remote records grouped by batch_id for the Industrial Cloud tab */
|
|
|
|
|
batchGroups: BatchSessionGroup[] = [];
|
|
batchGroups: BatchSessionGroup[] = [];
|
|
|
|
|
|
|
|
viewMode: 'local' | 'remote' = 'local';
|
|
viewMode: 'local' | 'remote' = 'local';
|
|
|
- loading = true;
|
|
|
|
|
|
|
+ loading = false;
|
|
|
expandedId: string | null = null;
|
|
expandedId: string | null = null;
|
|
|
|
|
|
|
|
- constructor(
|
|
|
|
|
- private localHistory: LocalHistoryService,
|
|
|
|
|
- private remoteInference: RemoteInferenceService,
|
|
|
|
|
- public visionSocket: VisionSocketService,
|
|
|
|
|
- ) {}
|
|
|
|
|
|
|
+ constructor() {
|
|
|
|
|
+ super(inject(Store));
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- ngOnInit(): void {
|
|
|
|
|
|
|
+ override ngOnInit(): void {
|
|
|
|
|
+ super.ngOnInit();
|
|
|
this.loadLocalHistory();
|
|
this.loadLocalHistory();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ override ngOnDestroy(): void {
|
|
|
|
|
+ this.batchGroups.forEach(g => g.records.forEach(r => { r.imageData = null; }));
|
|
|
|
|
+ super.ngOnDestroy();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
loadLocalHistory(): void {
|
|
loadLocalHistory(): void {
|
|
|
this.loading = true;
|
|
this.loading = true;
|
|
|
this.localHistoryRecords = this.localHistory.getRecords();
|
|
this.localHistoryRecords = this.localHistory.getRecords();
|
|
|
this.loading = false;
|
|
this.loading = false;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- loadRemoteHistory(): void {
|
|
|
|
|
|
|
+ async loadRemoteHistory(): Promise<void> {
|
|
|
this.loading = true;
|
|
this.loading = true;
|
|
|
- this.remoteInference.getHistory().subscribe({
|
|
|
|
|
- next: (data) => {
|
|
|
|
|
- this.remoteHistoryRecords = data.map(record => ({
|
|
|
|
|
- ...record,
|
|
|
|
|
- timestamp: new Date(record.created_at).toLocaleString(),
|
|
|
|
|
- engine: 'API AI',
|
|
|
|
|
- isNormalized: false,
|
|
|
|
|
- imageData: `${environment.apiUrl}/palm-oil/archive/${record.archive_id}`
|
|
|
|
|
- }));
|
|
|
|
|
- this.batchGroups = this.groupByBatch(this.remoteHistoryRecords);
|
|
|
|
|
- this.loading = false;
|
|
|
|
|
- },
|
|
|
|
|
- error: (err) => {
|
|
|
|
|
- console.error('Failed to load remote history:', err);
|
|
|
|
|
- this.loading = false;
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ const data: any = await firstValueFrom(
|
|
|
|
|
+ this.dpService.stream(this.buildFisMessage('History', 'getAll', {}))
|
|
|
|
|
+ );
|
|
|
|
|
+ const records = Array.isArray(data) ? data : [];
|
|
|
|
|
+ this.remoteHistoryRecords = records.map((record: any) => ({
|
|
|
|
|
+ ...record,
|
|
|
|
|
+ timestamp: new Date(record.created_at).toLocaleString(),
|
|
|
|
|
+ engine: 'Socket AI',
|
|
|
|
|
+ imageData: null,
|
|
|
|
|
+ }));
|
|
|
|
|
+ this.batchGroups = this.groupByBatch(this.remoteHistoryRecords);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('Failed to load remote history:', err);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.loading = false;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** Group remote records by batch_id. Records without a batch_id each form a singleton group. */
|
|
|
|
|
private groupByBatch(records: any[]): BatchSessionGroup[] {
|
|
private groupByBatch(records: any[]): BatchSessionGroup[] {
|
|
|
const map = new Map<string, any[]>();
|
|
const map = new Map<string, any[]>();
|
|
|
records.forEach(r => {
|
|
records.forEach(r => {
|
|
@@ -110,8 +120,27 @@ export class HistoryComponent implements OnInit {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- toggleBatchGroup(group: BatchSessionGroup): void {
|
|
|
|
|
|
|
+ async toggleBatchGroup(group: BatchSessionGroup): Promise<void> {
|
|
|
group.expanded = !group.expanded;
|
|
group.expanded = !group.expanded;
|
|
|
|
|
+ if (group.expanded) {
|
|
|
|
|
+ await this.loadImagesForGroup(group);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private async loadImagesForGroup(group: BatchSessionGroup): Promise<void> {
|
|
|
|
|
+ for (const record of group.records) {
|
|
|
|
|
+ // null = not fetched; false = failed; string = loaded — skip anything already attempted
|
|
|
|
|
+ if (record.imageData !== null) continue;
|
|
|
|
|
+ if (!record.archive_id) { record.imageData = false; continue; }
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res: any = await firstValueFrom(
|
|
|
|
|
+ this.dpService.stream(this.buildFisMessage('PalmHistory', 'GetImage', { archiveId: record.archive_id }))
|
|
|
|
|
+ );
|
|
|
|
|
+ record.imageData = res?.image_data ?? false;
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ record.imageData = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ── Delete actions ────────────────────────────────────────────────────────
|
|
// ── Delete actions ────────────────────────────────────────────────────────
|
|
@@ -129,20 +158,30 @@ export class HistoryComponent implements OnInit {
|
|
|
this.loadLocalHistory();
|
|
this.loadLocalHistory();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- deleteRemoteRecord(record: any, event: Event): void {
|
|
|
|
|
|
|
+ async deleteRemoteRecord(record: any, event: Event): Promise<void> {
|
|
|
event.stopPropagation();
|
|
event.stopPropagation();
|
|
|
if (!record.archive_id) return;
|
|
if (!record.archive_id) return;
|
|
|
- this.remoteInference.deleteRecord(record.archive_id).subscribe(() => {
|
|
|
|
|
- this.loadRemoteHistory();
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ await firstValueFrom(
|
|
|
|
|
+ this.dpService.stream(this.buildFisMessage('History', 'delete', { archiveId: record.archive_id }))
|
|
|
|
|
+ );
|
|
|
|
|
+ await this.loadRemoteHistory();
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('Failed to delete record:', err);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- clearRemoteHistory(event: Event): void {
|
|
|
|
|
|
|
+ async clearRemoteHistory(event: Event): Promise<void> {
|
|
|
event.stopPropagation();
|
|
event.stopPropagation();
|
|
|
if (!confirm('Delete ALL industrial cloud records from the server? This also removes archived images from disk.')) return;
|
|
if (!confirm('Delete ALL industrial cloud records from the server? This also removes archived images from disk.')) return;
|
|
|
- this.remoteInference.clearAll().subscribe(() => {
|
|
|
|
|
- this.loadRemoteHistory();
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ await firstValueFrom(
|
|
|
|
|
+ this.dpService.stream(this.buildFisMessage('History', 'clearAll', {}))
|
|
|
|
|
+ );
|
|
|
|
|
+ await this.loadRemoteHistory();
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('Failed to clear history:', err);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
get currentHistory(): any[] {
|
|
get currentHistory(): any[] {
|
|
@@ -186,12 +225,10 @@ export class HistoryComponent implements OnInit {
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** CSS border color for a detection box by ripeness class */
|
|
|
|
|
getDetectionColor(det: any): string {
|
|
getDetectionColor(det: any): string {
|
|
|
return GRADE_COLORS[det.class] ?? '#00A651';
|
|
return GRADE_COLORS[det.class] ?? '#00A651';
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** Aggregate industrial_summary for a batch group into display badges */
|
|
|
|
|
getGroupSummary(group: BatchSessionGroup): string[] {
|
|
getGroupSummary(group: BatchSessionGroup): string[] {
|
|
|
const totals: Record<string, number> = {};
|
|
const totals: Record<string, number> = {};
|
|
|
group.records.forEach(r => {
|
|
group.records.forEach(r => {
|
|
@@ -206,4 +243,20 @@ export class HistoryComponent implements OnInit {
|
|
|
.filter(([, n]) => n > 0)
|
|
.filter(([, n]) => n > 0)
|
|
|
.map(([cls, n]) => `${cls}: ${n}`);
|
|
.map(([cls, n]) => `${cls}: ${n}`);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ private buildFisMessage(serviceId: string, messageName: string, data: any): FisAppMessage {
|
|
|
|
|
+ return {
|
|
|
|
|
+ header: {
|
|
|
|
|
+ messageType: 'Command' as any,
|
|
|
|
|
+ messageID: crypto.randomUUID(),
|
|
|
|
|
+ messageName,
|
|
|
|
|
+ dateCreated: new Date().toISOString(),
|
|
|
|
|
+ isAggregate: false,
|
|
|
|
|
+ serviceId,
|
|
|
|
|
+ messageProducerInformation: { producerName: 'palm-oil-ai' } as any,
|
|
|
|
|
+ security: { ucpId: 'palm-oil-ai' },
|
|
|
|
|
+ },
|
|
|
|
|
+ data,
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|