Compare commits
4 Commits
master
...
feature/ad
| Author | SHA1 | Date | |
|---|---|---|---|
| 67ee8b86b8 | |||
| 0e023e59f0 | |||
| b987b48a5f | |||
| 045ce34398 |
@ -12,7 +12,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
|
||||
jobs:
|
||||
push-to-svn:
|
||||
runs-on: self-hosted
|
||||
@ -44,7 +44,7 @@ jobs:
|
||||
--no-auth-cache \
|
||||
--non-interactive \
|
||||
--trust-server-cert \
|
||||
"${{ secrets.SVN_REPO_URL }}/branches/data-export-api-copy" svn-branch
|
||||
"${{ secrets.SVN_REPO_URL }}/branches/advanced-reports" svn-branch
|
||||
|
||||
- name: Sync files to SVN working copy
|
||||
run: |
|
||||
@ -74,4 +74,4 @@ jobs:
|
||||
--no-auth-cache \
|
||||
--non-interactive \
|
||||
--trust-server-cert \
|
||||
-m "Gitea CI sync from commit ${{ github.sha }} [ci skip]"
|
||||
-m "Gitea CI sync from commit ${{ github.sha }} [ci skip]"
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
CURRENT_FILE_PATH=src/locale/messages.xlf
|
||||
TRANSLATED_FILE_PATH_ES=src/locale/messages.es.xlf
|
||||
TRANSLATED_FILE_PATH_PT=src/locale/messages.pt.xlf
|
||||
GOOGLE_LOCATION=global
|
||||
GOOGLE_PROJECT_ID=predictive-fx-392018
|
||||
GOOGLE_APPLICATION_CREDENTIALS=google-cloud.json
|
||||
@ -1,6 +0,0 @@
|
||||
/*
|
||||
To disable deselect or reselect an item on a table row
|
||||
Ref:https://stackoverflow.com/questions/48675497/how-to-disable-the-option-to-deselect-a-row-on-turbotable-component*/
|
||||
tr.ui-state-highlight {
|
||||
pointer-events: none;
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
<div class="ui-g">
|
||||
<div class="ui-g-12">
|
||||
<div class="card">
|
||||
<p-table #dt [value]="clients" [columns]="cols" selectionMode="single" (onRowSelect)="onRowSelect($event)" [paginator]="true" [rows]="15" [pageLinks]="5" [rowsPerPageOptions]="[15,30,50]" [alwaysShowPaginator]="false" [(selection)]="currClient" dataKey="_id" [resetPageOnSort]="false" stateStorage="session" stateKey="cltb-ops" [responsive]="true">
|
||||
<ng-template pTemplate="caption">
|
||||
<span class="table-caption-1" i18n="@@clientList">Client List</span>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="header" let-columns>
|
||||
<tr>
|
||||
<th *ngFor="let col of columns" [pSortableColumn]="col.field" [width]="col.width">{{col.header}}<p-sortIcon [field]="col.field"></p-sortIcon>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th *ngFor="let col of columns" [ngSwitch]="col.filtered" class="ui-fluid">
|
||||
<div class="input-with-icon" *ngSwitchCase="true">
|
||||
<i class="ui-icon-search"></i>
|
||||
<input pInputText type="text" (input)="dt.filter($event.target.value, col.field, col.filterMatchMode)" [value]="dt.filters[col.field]?.value">
|
||||
</div>
|
||||
<span *ngSwitchDefault></span>
|
||||
</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="body" let-client let-rowData let-columns>
|
||||
<tr [pSelectableRow]="client">
|
||||
<td *ngFor="let col of cols">
|
||||
<span class="ui-column-title">{{col.header}}</span>
|
||||
{{resolveFieldData(rowData, col.field)}}
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
<div class="ui-widget-header ui-helper-clearfix toolbar">
|
||||
<button type="button" *ngIf="canWrite" pButton icon="ui-icon-plus" (click)="newClient()" i18n-label="@@new" label="New"></button>
|
||||
<button type="button" pButton icon="ui-icon-edit" [disabled]="!canEdit" (click)="editClient()" i18n-label="@@detail" label="Detail"></button>
|
||||
<button type="button" *ngIf="canWrite" [disabled]="!canEdit" pButton icon="ui-icon-trash" (click)="deleteClient()" i18n-label="@@delete" label="Delete"></button>
|
||||
<div class="float-right">
|
||||
<button type="button" [disabled]="!canEdit" pButton icon="ui-icon-view-list" (click)="toJobList()" i18n-label="@@viewJobs" label="View Jobs"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,111 +0,0 @@
|
||||
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { Table } from 'primeng/table';
|
||||
|
||||
import { Client } from '../models/client.model';
|
||||
import * as fromClients from '../reducers';
|
||||
import * as clientActions from '../actions/client.actions';
|
||||
|
||||
import { RoleIds, globals } from '../../shared/global';
|
||||
import { JobService } from '../../domain/services/job.service';
|
||||
import { Utils } from 'src/app/shared/utils';
|
||||
import { BaseComp } from 'src/app/shared/base/base.component';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'agm-client-list',
|
||||
templateUrl: './client-list.component.html',
|
||||
styleUrls: ['./client-list.component.css']
|
||||
})
|
||||
export class ClientListComponent extends BaseComp implements OnInit, OnDestroy {
|
||||
resolveFieldData = Utils.resolveFieldData;
|
||||
|
||||
clients: Array<Client>;
|
||||
currClient: Client;
|
||||
|
||||
@ViewChild('dt') dt: Table;
|
||||
|
||||
cols: any[];
|
||||
loading$ = this.store.select(fromClients.isLoading);
|
||||
|
||||
get canWrite(): boolean {
|
||||
return this.authSvc.hasRole([RoleIds.APP, RoleIds.APP_ADM, RoleIds.OFFICER]);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly jobService: JobService,
|
||||
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.cols = [
|
||||
{ field: 'name', header: globals.name, filtered: true, filterMatchMode: 'contains' },
|
||||
{ field: 'username', header: globals.userName, filtered: true, filterMatchMode: 'contains' },
|
||||
{ field: 'address', header: globals.address },
|
||||
{ field: 'contact', header: globals.contact, filtered: true },
|
||||
{ field: 'phone', header: globals.phone + ' ' + $localize`:@@Num:N°`, width: '15%', filtered: true },
|
||||
{ field: 'email', header: globals.email, filtered: true, filterMatchMode: 'contains' }
|
||||
];
|
||||
|
||||
// These reference entity services and logic should apply to logged in logic later
|
||||
this.sub$ = this.store.select(fromClients.getAllClients).subscribe(clients => this.clients = clients);
|
||||
|
||||
this.sub$.add(this.store.select(fromClients.getSelectedClient).subscribe((client) => {
|
||||
this.currClient = client;
|
||||
}));
|
||||
|
||||
this.store.dispatch(new clientActions.Fetch());
|
||||
}
|
||||
|
||||
onRowSelect(event) {
|
||||
this.store.dispatch(new clientActions.Select(event.data));
|
||||
}
|
||||
|
||||
get canEdit() {
|
||||
return (this.currClient && this.currClient._id !== '0');
|
||||
}
|
||||
|
||||
newClient() {
|
||||
this.router.navigate(['client', '0'], { relativeTo: this.route });
|
||||
}
|
||||
|
||||
editClient() {
|
||||
this.router.navigate(['client', this.currClient._id], { relativeTo: this.route });
|
||||
}
|
||||
|
||||
deleteClient() {
|
||||
this.jobService.countByClient(this.currClient._id).subscribe((count) => {
|
||||
return this.confirmDelete(count);
|
||||
});
|
||||
}
|
||||
|
||||
private confirmDelete(count: number) {
|
||||
let msg = globals.confirmDeleteThing.replace('#thing#', globals.client);
|
||||
if (count > 0) {
|
||||
let ref = count === 1 ? $localize`:@@relatedJobSingular:There is #jobs# related job` : $localize`:@@relatedJobPlural:There are #jobs# related jobs`;
|
||||
ref = ref.replace('#jobs#', String(count));
|
||||
msg = ref + '. ' + msg;
|
||||
}
|
||||
|
||||
this.confirmSvc.confirm({
|
||||
message: msg,
|
||||
accept: () => {
|
||||
this.store.dispatch(new clientActions.Delete(this.currClient));
|
||||
this.currClient = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toJobList() {
|
||||
this.router.navigate(['/jobs']);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
super.ngOnDestroy();
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
.pure-white {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'agm-dashboard',
|
||||
templateUrl: './dashboard.component.html',
|
||||
styleUrls: ['./dashboard.component.css']
|
||||
})
|
||||
export class DashboardComponent {
|
||||
|
||||
constructor() { }
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
.export-item {
|
||||
border: 1px solid #bdbdbd;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
cursor:pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.export-item:hover {
|
||||
color: #fff;
|
||||
border: 1px solid #4caf50;
|
||||
background-color: #4caf50;
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
@media (max-width: 767px) {
|
||||
.ui-sm-12.no-pad {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.inline-flex-end {
|
||||
display: inline-flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
:host ::ng-deep .ui-calendar input,
|
||||
:host ::ng-deep .ui-calendar .ui-datepicker-trigger {
|
||||
opacity: 0;
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
pointer-events: auto;
|
||||
}
|
||||
@ -1,628 +0,0 @@
|
||||
import { Component, OnInit, OnDestroy, ViewChild, AfterViewInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { Subscription, interval } from 'rxjs';
|
||||
|
||||
import { SelectItem } from 'primeng/api';
|
||||
import { Dropdown } from 'primeng/dropdown';
|
||||
import { Table } from 'primeng/table';
|
||||
|
||||
import { IUIJob } from '../models/job.model';
|
||||
import * as jobActions from '../actions/job.actions';
|
||||
import * as clientActions from '@app/client/actions/client.actions';
|
||||
|
||||
import { select } from '@ngrx/store';
|
||||
import * as fromJobs from '../reducers/';
|
||||
|
||||
import * as fromClients from '@app/client/reducers';
|
||||
|
||||
import { GC, RoleIds, globals, jobInvoiceStatus, jobListStatus, locales } from '@app/shared/global';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { Client } from '@app/client/models/client.model';
|
||||
import { BaseComp } from '@app/shared/base/base.component';
|
||||
import { Utils } from '@app/shared/utils';
|
||||
import { selectLimit } from '@app/reducers';
|
||||
import { Acre } from '@app/domain/models/subscription.model';
|
||||
import { SUB, SubTexts, SubType } from '@app/profile/common';
|
||||
import { InvoiceService } from '@app/domain/services/invoice.service';
|
||||
import { RestoreTableState } from '@app/shared/restore-table-state';
|
||||
import { SubscriptionService } from '@app/domain/services/subscription.service';
|
||||
import { GAService } from '@app/shared/ga.service';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'agm-job-list',
|
||||
templateUrl: './job-list.component.html',
|
||||
styleUrls: ['./job-list.component.css']
|
||||
})
|
||||
export class JobListComponent extends BaseComp implements OnInit, AfterViewInit, OnDestroy {
|
||||
globals = globals;
|
||||
readonly dropdownStyle = { 'min-width': '170px', 'color': 'black' };
|
||||
readonly customeDate = 'customDate';
|
||||
|
||||
jobs: Array<IUIJob> = [];
|
||||
currentJob: IUIJob;
|
||||
currClient: SelectItem;
|
||||
clients: SelectItem[];
|
||||
defaultInvoiceSetting;
|
||||
|
||||
@ViewChild('dt') public dt: Table;
|
||||
@ViewChild('cl') public cl: Dropdown;
|
||||
@ViewChild('calendar') calendar: any;
|
||||
|
||||
rows1Page = [10, 15, 30, 60, 100];
|
||||
cols: any[];
|
||||
|
||||
status: SelectItem[] = [GC.selAll, ...GC.selJobStatuses];
|
||||
statusFilter;
|
||||
reloadOps: SelectItem[];
|
||||
reloadBy = 0;
|
||||
reload$: Subscription;
|
||||
showStatusPlus: boolean;
|
||||
|
||||
totalJobs;
|
||||
|
||||
acre: Acre;
|
||||
dateOptions: {
|
||||
label: string;
|
||||
value: string;
|
||||
}[];
|
||||
selDate: string;
|
||||
|
||||
selCalDate: [Date, Date];
|
||||
|
||||
get canWrite(): boolean {
|
||||
return this.authSvc.hasRole([RoleIds.APP, RoleIds.APP_ADM, RoleIds.OFFICER, RoleIds.PILOT, RoleIds.CLIENT]);
|
||||
}
|
||||
|
||||
get canWriteInvoice(): boolean {
|
||||
return this.authSvc.canAccessInvoice
|
||||
&& this.jobs?.length > 0;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly datePipe: DatePipe,
|
||||
private readonly invoiceSvc: InvoiceService,
|
||||
private readonly restoreTableSvc: RestoreTableState,
|
||||
private readonly subscriptionService: SubscriptionService,
|
||||
private readonly gaService: GAService
|
||||
) {
|
||||
super();
|
||||
this.currClient = ({ label: globals.all, value: null });
|
||||
this.totalJobs = { '=0': '', '=1': '1 ' + $localize`:@@job:job`.toLocaleLowerCase(), 'other': $localize`:@@total#Jobs:Total: # jobs` };
|
||||
|
||||
this.status = [
|
||||
{ label: globals.all, value: jobListStatus.ALL },
|
||||
{ label: globals.statusNew, value: jobListStatus.NEW },
|
||||
{ label: globals.statusReady, value: jobListStatus.READY },
|
||||
{ label: globals.statusDownloaded, value: jobListStatus.DOWNLOAD },
|
||||
{ label: globals.statusSprayed, value: jobListStatus.SPRAY },
|
||||
{ label: globals.statusInvoiced, value: jobListStatus.INVOICED },
|
||||
];
|
||||
|
||||
this.statusFilter = jobListStatus.ALL;
|
||||
|
||||
this.cols = [
|
||||
{ field: '_id', header: $localize`:@@id:Id` + ' ' + globals.num, width: '10%', filtered: true, filterMatchMode: 'contains' },
|
||||
{
|
||||
field: 'orderNumber',
|
||||
header: $localize`:@@order:Order` + ' ' + globals.num,
|
||||
width: '10%',
|
||||
filtered: true,
|
||||
filterMatchMode: 'contains'
|
||||
},
|
||||
{ field: 'name', header: globals.name, width: this.isClientUser ? '34%' : '20%', filtered: true, filterMatchMode: 'contains' },
|
||||
{ field: 'startDate', header: $localize`:@@startDate:Start Date`, width: '12%' },
|
||||
{ field: 'endDate', header: $localize`:@@endDate:End Date`, width: '12%' },
|
||||
{ field: 'status', header: $localize`:@@status:Status`, width: '22%' },
|
||||
];
|
||||
if (!this.isClientUser) {
|
||||
this.cols.unshift({ field: 'client.name', header: $localize`:@@client:Client`, width: '14%' });
|
||||
}
|
||||
|
||||
this.reloadOps = [
|
||||
{ label: globals.noReload, value: 0 },
|
||||
{ label: globals.reloadByMinutes.replace('#count#', '5'), value: 5 },
|
||||
{ label: globals.reloadByMinutes.replace('#count#', '10'), value: 10 },
|
||||
{ label: globals.reloadByMinutes.replace('#count#', '15'), value: 15 }
|
||||
];
|
||||
this.showStatusPlus = !this.authSvc.hasRole([RoleIds.CLIENT, RoleIds.INSPECTOR]);
|
||||
this.defaultInvoiceSetting = this.invoiceSvc.defaultSetting;
|
||||
|
||||
this.dateOptions = this.subscriptionService.getDateOptions();
|
||||
this.dateOptions.push({ label: $localize`:@@customDate:Custom Date`, value: this.customeDate });
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
// Initialize subscriptions first to get accurate data
|
||||
this.sub$ = this.store.pipe(select(fromClients.getAllClients)).subscribe(clients => {
|
||||
if (Utils.isEmptyArray(clients)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.clients = clients.map(it => ({ value: it._id, label: it.name }));
|
||||
if (!this.isClientUser) {
|
||||
this.clients.unshift(({ label: globals.all, value: null }));
|
||||
}
|
||||
});
|
||||
this.sub$.add(this.store.pipe(select(fromClients.getSelectedClient)).subscribe(client => {
|
||||
if (client) {
|
||||
if (this.currClient.value !== client._id) {
|
||||
this.currClient = ({ label: client.name, value: client._id });
|
||||
}
|
||||
} else {
|
||||
this.currClient = ({ label: globals.all, value: null });
|
||||
}
|
||||
})); this.sub$.add(this.store.pipe(select(fromJobs.getJobsByClient)).subscribe(jobs => {
|
||||
this.jobs = jobs;
|
||||
}));
|
||||
this.sub$.add(this.store.pipe(select(fromJobs.getSelectedJob)).subscribe((job) => {
|
||||
this.currentJob = job;
|
||||
}));
|
||||
|
||||
this.sub$.add(this.store.select(selectLimit(SubType.PACKAGE)).subscribe((pkg) => {
|
||||
if (pkg) {
|
||||
const lookupKey = this.authSvc.getCurLookupKey(SubType.PACKAGE);
|
||||
|
||||
// If lookup key is empty (user data not loaded yet), find first package key
|
||||
let effectiveLookupKey = lookupKey;
|
||||
if (!lookupKey && pkg) {
|
||||
const packageKeys = Object.keys(pkg);
|
||||
if (packageKeys.length > 0) {
|
||||
effectiveLookupKey = packageKeys[0]; // Use first available package
|
||||
}
|
||||
}
|
||||
|
||||
this.acre = pkg[effectiveLookupKey]?.acre;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
// Track job list viewed ONCE when component is fully initialized
|
||||
this.trackJobListViewedEvent();
|
||||
|
||||
const listFilter = sessionStorage.getItem('jtb-ops') ? JSON.parse(sessionStorage.getItem('jtb-ops')) : null;
|
||||
|
||||
if (listFilter?.filters) {
|
||||
const status = listFilter.filters.status?.value;
|
||||
const invoiced = listFilter.filters.invoiceStatus?.value;
|
||||
this.restoreStatusState(status, invoiced);
|
||||
}
|
||||
const storedDateSelection = sessionStorage.getItem('jobListSelDate');
|
||||
if (storedDateSelection) {
|
||||
const parsedDateSelection = JSON.parse(storedDateSelection);
|
||||
if (parsedDateSelection.selDate) {
|
||||
this.selDate = parsedDateSelection.selDate;
|
||||
} else {
|
||||
if (parsedDateSelection.selCalDate) {
|
||||
if (parsedDateSelection.selCalDate[0] && parsedDateSelection.selCalDate[1]) {
|
||||
this.selCalDate = [new Date(parsedDateSelection.selCalDate[0]), new Date(parsedDateSelection.selCalDate[1])];
|
||||
} else {
|
||||
this.selCalDate = [new Date(parsedDateSelection.selCalDate[0]), null];
|
||||
}
|
||||
}
|
||||
this.selDate = this.selCalDate ? this.customeDate : this.dateOptions[0].value;
|
||||
this.setCustomDateLabel();
|
||||
}
|
||||
}
|
||||
if (this.cl) {
|
||||
this.cl.registerOnChange((newVal) => {
|
||||
this.store.dispatch(new clientActions.Select(<Client>({ _id: newVal.value })));
|
||||
this.fetchJobsByClient(this.currClient.value);
|
||||
this.dt.first = 0;
|
||||
});
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.fetchJobsByClient(this.currClient.value);
|
||||
if (this.dt.rows >= this.dt.totalRecords) {
|
||||
this.dt.first = 0;
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
private trackJobListViewedEvent(): void {
|
||||
// Track agricultural business intelligence (complements automatic page_view)
|
||||
this.gaService.trackJobListViewed({
|
||||
user_id: this.authSvc.user?._id || 'anonymous',
|
||||
platform: 'web',
|
||||
view_type: 'table',
|
||||
total_jobs: this.jobs?.length || 0,
|
||||
displayed_jobs: this.jobs?.length || 0,
|
||||
sort_by: this.dt?.sortField || null,
|
||||
filter_count: this.getActiveFilterCount(),
|
||||
client_filter_applied: !!this.currClient?.value,
|
||||
reload_interval: this.reloadBy
|
||||
});
|
||||
}
|
||||
|
||||
restoreStatusState(status, invoiced) {
|
||||
const statusMap = {
|
||||
0: jobListStatus.NEW,
|
||||
1: jobListStatus.READY,
|
||||
2: jobListStatus.DOWNLOAD,
|
||||
3: jobListStatus.SPRAY,
|
||||
[jobInvoiceStatus.INVOICED]: jobListStatus.INVOICED
|
||||
};
|
||||
this.statusFilter = statusMap[status] ?? statusMap[invoiced] ?? jobListStatus.ALL;
|
||||
}
|
||||
|
||||
fetchJobsByClient(clientId) {
|
||||
const statusMap = {
|
||||
[jobListStatus.ALL]: jobListStatus.ALL,
|
||||
[jobListStatus.NEW]: 0,
|
||||
[jobListStatus.READY]: 1,
|
||||
[jobListStatus.DOWNLOAD]: 2,
|
||||
[jobListStatus.SPRAY]: 3,
|
||||
[jobListStatus.INVOICED]: jobInvoiceStatus.INVOICED
|
||||
};
|
||||
|
||||
const byTime =
|
||||
this.selDate
|
||||
? this.selDate == this.customeDate
|
||||
? this.selCalDate
|
||||
: [this.selDate]
|
||||
: [this.dateOptions[0].value];
|
||||
|
||||
const statusValue = statusMap[this.statusFilter] ?? jobListStatus.ALL;
|
||||
this.store.dispatch(new jobActions.Fetch({
|
||||
clientId: clientId,
|
||||
jobsByPilot: (this.authSvc.isPilotUser && this.settings.jobsByPilot),
|
||||
byTime,
|
||||
status: statusValue
|
||||
}));
|
||||
}
|
||||
|
||||
restoreTableFirst() {
|
||||
this.restoreTableSvc.restoreTableFirst(this.dt);
|
||||
}
|
||||
|
||||
onPageChange(e) {
|
||||
this.restoreTableSvc.onPageChange(this.dt, e);
|
||||
}
|
||||
|
||||
onRowSelect(event) {
|
||||
this.store.dispatch(new jobActions.Select(this.currentJob));
|
||||
|
||||
// Track job selection
|
||||
if (this.currentJob) {
|
||||
const positionInList = this.jobs.findIndex(job => job._id === this.currentJob._id) + 1;
|
||||
|
||||
this.gaService.trackJobSelected({
|
||||
user_id: this.authSvc.user?._id || 'anonymous',
|
||||
platform: 'web',
|
||||
job_id: this.currentJob._id.toString(),
|
||||
selection_method: 'row_click',
|
||||
position_in_list: positionInList,
|
||||
job_type: this.currentJob.appType || 'unknown',
|
||||
job_status: this.currentJob.status?.toString() || 'unknown'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get canAddNew(): boolean {
|
||||
// Check subscription package loaded (!!this.acre) and not over limit
|
||||
// Note: With unlimited acres (limit: null), overLimit will always be false,
|
||||
// but keep this check for defensive programming in case limited plans return
|
||||
return !!this.acre && !this.acre.overLimit;
|
||||
}
|
||||
|
||||
displaySubDia() {
|
||||
return this.confirmSvc.confirm({
|
||||
header: SubTexts.textUpgradeSub,
|
||||
message: SubTexts.textUpgradeSubMsg,
|
||||
accept: () => {
|
||||
this.router.navigate([SUB.PROFILE, SUB.MY_SERVICES]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
newJob() {
|
||||
if (this.canAddNew) {
|
||||
return this.router.navigate(['./0/edit'], { relativeTo: this.route });
|
||||
}
|
||||
return this.displaySubDia();
|
||||
}
|
||||
|
||||
duplicateJob() {
|
||||
if (this.canAddNew) {
|
||||
// Track bulk action (duplicate)
|
||||
this.gaService.trackJobBulkAction({
|
||||
user_id: this.authSvc.user?._id || 'anonymous',
|
||||
platform: 'web',
|
||||
action_type: 'duplicate',
|
||||
job_count: 1,
|
||||
job_ids: [this.currentJob._id.toString()],
|
||||
success_rate: 1.0
|
||||
});
|
||||
|
||||
return this.router.navigate([`./${this.currentJob._id}/edit`, { dup: true }], { relativeTo: this.route });
|
||||
}
|
||||
return this.displaySubDia();
|
||||
}
|
||||
|
||||
editJob() {
|
||||
this.router.navigate([`./${this.currentJob._id}/edit`], { relativeTo: this.route });
|
||||
}
|
||||
|
||||
editJobMap() {
|
||||
this.router.navigate([`./${this.currentJob._id}/editMap`, { flag: 0 }], { relativeTo: this.route });
|
||||
}
|
||||
|
||||
canEdit() {
|
||||
return (this.currentJob && this.currentJob._id !== 0);
|
||||
}
|
||||
|
||||
canCreateInvoice() {
|
||||
return (this.currentJob &&
|
||||
this.currentJob.status != 0 &&
|
||||
this.currentJob.costings &&
|
||||
this.currentJob.costings.billableAmount &&
|
||||
this.currentJob.invoiceStatus == jobInvoiceStatus.NONE);
|
||||
}
|
||||
|
||||
reloadJobs() {
|
||||
const startTime = performance.now();
|
||||
|
||||
this.fetchJobsByClient(this.currClient && this.currClient.value);
|
||||
|
||||
// Track job list reload
|
||||
setTimeout(() => {
|
||||
const endTime = performance.now();
|
||||
this.gaService.trackJobListViewed({
|
||||
user_id: this.authSvc.user?._id || 'anonymous',
|
||||
platform: 'web',
|
||||
view_type: 'table',
|
||||
total_jobs: this.jobs?.length || 0,
|
||||
displayed_jobs: this.jobs?.length || 0,
|
||||
sort_by: this.dt?.sortField || null,
|
||||
filter_count: this.getActiveFilterCount(),
|
||||
load_time_ms: Math.round(endTime - startTime),
|
||||
client_filter_applied: !!this.currClient?.value,
|
||||
reload_interval: this.reloadBy
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
reloadChanged(value) {
|
||||
if (this.reload$) {
|
||||
this.reload$.unsubscribe();
|
||||
}
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
this.reload$ = interval(value * 60 * 1000).subscribe(() => this.reloadJobs());
|
||||
}
|
||||
|
||||
deleteJob() {
|
||||
this.confirmSvc.confirm({
|
||||
message: globals.confirmDeleteThing.replace('#thing#', globals.job),
|
||||
accept: () => {
|
||||
this.store.dispatch(new jobActions.Delete(this.currentJob));
|
||||
this.currentJob = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createInvoice() {
|
||||
if (!this.defaultInvoiceSetting) {
|
||||
this.msgSvc.addFailedMsg($localize`:@@noInvoiceSettingOnCreateInvoiceErr:Please create invoice setting before create invoice`);
|
||||
return;
|
||||
}
|
||||
if (this.defaultInvoiceSetting && this.currentJob.costings.currency != this.defaultInvoiceSetting.currency) {
|
||||
this.msgSvc.addFailedMsg($localize`:@@jobCurrencyNotMatchSettingErr:This job's currency does not match with invoice currency setting.`);
|
||||
return;
|
||||
}
|
||||
this.router.navigate(['/invoices/edit/0']);
|
||||
}
|
||||
|
||||
gotoClients() {
|
||||
this.router.navigate(['/clients']);
|
||||
}
|
||||
|
||||
getUsers(byUsers) {
|
||||
if (!byUsers || !Array.isArray(byUsers) || byUsers.length === 0) {
|
||||
return '';
|
||||
}
|
||||
let byStr = '';
|
||||
for (let i = 0; i < byUsers.length; i++) {
|
||||
const it = byUsers[i];
|
||||
byStr += `${it.user} - ${this.datePipe.transform(it.date, 'MMM.dd')}`;
|
||||
if (i !== byUsers.length - 1) {
|
||||
byStr += ', ';
|
||||
}
|
||||
}
|
||||
return $localize`:@@by:by` + ': <br/>' + byStr;
|
||||
}
|
||||
|
||||
handleStatusFilter(value) {
|
||||
const previousCount = this.jobs?.length || 0;
|
||||
|
||||
switch (value) {
|
||||
case jobListStatus.ALL:
|
||||
this.dt.filter(null, 'status', 'equals');
|
||||
this.dt.filter('', 'invoiceStatus', 'contains');
|
||||
this.statusFilter = jobListStatus.ALL;
|
||||
break;
|
||||
case jobListStatus.NEW:
|
||||
this.dt.filter(0, 'status', 'equals');
|
||||
this.dt.filter('', 'invoiceStatus', 'contains');
|
||||
this.statusFilter = jobListStatus.NEW;
|
||||
break;
|
||||
case jobListStatus.READY:
|
||||
this.dt.filter(1, 'status', 'equals');
|
||||
this.dt.filter('', 'invoiceStatus', 'contains');
|
||||
this.statusFilter = jobListStatus.READY;
|
||||
break;
|
||||
case jobListStatus.DOWNLOAD:
|
||||
this.dt.filter(2, 'status', 'equals');
|
||||
this.dt.filter('', 'invoiceStatus', 'contains');
|
||||
this.statusFilter = jobListStatus.DOWNLOAD;
|
||||
break;
|
||||
case jobListStatus.SPRAY:
|
||||
this.dt.filter(3, 'status', 'equals');
|
||||
this.dt.filter('', 'invoiceStatus', 'contains');
|
||||
this.statusFilter = jobListStatus.SPRAY;
|
||||
break;
|
||||
case jobListStatus.INVOICED:
|
||||
this.dt.filter(null, 'status', 'equals');
|
||||
this.dt.filter(jobInvoiceStatus.INVOICED, 'invoiceStatus', 'contains');
|
||||
this.statusFilter = jobListStatus.INVOICED;
|
||||
break;
|
||||
}
|
||||
|
||||
// Track filter usage
|
||||
setTimeout(() => {
|
||||
const currentCount = this.jobs?.length || 0;
|
||||
this.gaService.trackJobListFiltered({
|
||||
user_id: this.authSvc.user?._id || 'anonymous',
|
||||
platform: 'web',
|
||||
filter_type: 'status',
|
||||
filter_value: value,
|
||||
results_before: previousCount,
|
||||
results_after: currentCount,
|
||||
filter_effectiveness: previousCount > 0 ? (currentCount / previousCount) : 0
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
private setJobListSelDate(dateSelection): void {
|
||||
sessionStorage.setItem('jobListSelDate', JSON.stringify(dateSelection));
|
||||
}
|
||||
|
||||
private setCustomDateLabel(): void {
|
||||
const dateFormat = this.locale.dateFormat.replace(/(^|\/)mm(\/|$)/g, '$1MM$2');
|
||||
|
||||
if (!this.selCalDate) {
|
||||
this.dateOptions.find(it => it.value === this.customeDate).label = $localize`:@@customDate:Custom Date`;
|
||||
} else if (!this.selCalDate[1]) {
|
||||
this.dateOptions.find(it => it.value === this.customeDate).label =
|
||||
`${this.datePipe.transform(this.selCalDate[0], dateFormat)}`;
|
||||
} else {
|
||||
this.dateOptions.find(it => it.value === this.customeDate).label =
|
||||
`${this.datePipe.transform(this.selCalDate[0], dateFormat)} - ${this.datePipe.transform(this.selCalDate[1], dateFormat)}`;
|
||||
}
|
||||
}
|
||||
|
||||
onDropdownChange(evt): void {
|
||||
const previousCount = this.jobs?.length || 0;
|
||||
|
||||
if (evt.value === this.customeDate) {
|
||||
setTimeout(() => this.showCal());
|
||||
} else {
|
||||
this.setJobListSelDate({ selDate: evt.value, selCalDate: null });
|
||||
this.reloadJobs();
|
||||
|
||||
// Track date filter usage
|
||||
setTimeout(() => {
|
||||
const currentCount = this.jobs?.length || 0;
|
||||
this.gaService.trackJobListFiltered({
|
||||
user_id: this.authSvc.user?._id || 'anonymous',
|
||||
platform: 'web',
|
||||
filter_type: 'date',
|
||||
filter_value: evt.value,
|
||||
results_before: previousCount,
|
||||
results_after: currentCount,
|
||||
filter_effectiveness: previousCount > 0 ? (currentCount / previousCount) : 0,
|
||||
date_filter_type: this.getDateFilterType(evt.value)
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
onCalClose(): void {
|
||||
const previousCount = this.jobs?.length || 0;
|
||||
|
||||
this.setCustomDateLabel();
|
||||
if (this.selCalDate) {
|
||||
this.setJobListSelDate({ selDate: null, selCalDate: this.selCalDate });
|
||||
} else {
|
||||
this.selDate = this.dateOptions[0].value;
|
||||
this.setJobListSelDate({ selDate: this.selDate, selCalDate: null });
|
||||
}
|
||||
this.reloadJobs();
|
||||
|
||||
// Track custom date filter usage
|
||||
if (this.selCalDate) {
|
||||
setTimeout(() => {
|
||||
const currentCount = this.jobs?.length || 0;
|
||||
this.gaService.trackJobListFiltered({
|
||||
user_id: this.authSvc.user?._id || 'anonymous',
|
||||
platform: 'web',
|
||||
filter_type: 'date',
|
||||
filter_value: 'custom_date_range',
|
||||
results_before: previousCount,
|
||||
results_after: currentCount,
|
||||
filter_effectiveness: previousCount > 0 ? (currentCount / previousCount) : 0,
|
||||
date_filter_type: 'custom',
|
||||
custom_date_range: [
|
||||
this.selCalDate[0]?.toISOString().split('T')[0],
|
||||
this.selCalDate[1]?.toISOString().split('T')[0]
|
||||
]
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
onCalClick() {
|
||||
setTimeout(() => this.showCal());
|
||||
}
|
||||
|
||||
showCal() {
|
||||
this.calendar?.el.nativeElement.querySelector('button')?.click();
|
||||
}
|
||||
|
||||
isShowXBtn(item) {
|
||||
return item?.value == this.customeDate && this.selDate == this.customeDate
|
||||
}
|
||||
|
||||
// Helper method to count active filters
|
||||
private getActiveFilterCount(): number {
|
||||
let count = 0;
|
||||
|
||||
// Check status filter
|
||||
if (this.statusFilter && this.statusFilter !== jobListStatus.ALL) {
|
||||
count++;
|
||||
}
|
||||
|
||||
// Check client filter
|
||||
if (this.currClient?.value) {
|
||||
count++;
|
||||
}
|
||||
|
||||
// Check date filter
|
||||
if (this.selDate && this.selDate !== this.dateOptions[0]?.value) {
|
||||
count++;
|
||||
}
|
||||
|
||||
// Check table column filters
|
||||
if (this.dt?.filters) {
|
||||
Object.keys(this.dt.filters).forEach(key => {
|
||||
const filter = this.dt.filters[key];
|
||||
if (filter && filter.value && filter.value !== '') {
|
||||
count++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
// Helper method to determine date filter type
|
||||
private getDateFilterType(value: string): 'today' | 'week' | 'month' | 'quarter' | 'custom' {
|
||||
if (value === this.customeDate) return 'custom';
|
||||
if (value?.includes('today')) return 'today';
|
||||
if (value?.includes('week')) return 'week';
|
||||
if (value?.includes('month')) return 'month';
|
||||
if (value?.includes('quarter')) return 'quarter';
|
||||
return 'custom';
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
super.ngOnDestroy();
|
||||
if (this.reload$) {
|
||||
this.reload$.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
.weather-info {
|
||||
padding-top: 0.25em;
|
||||
margin-top : 1em;
|
||||
}
|
||||
|
||||
.data-detail-box {
|
||||
border: 1px solid lightgray;
|
||||
}
|
||||
|
||||
.speed-slider {
|
||||
padding: 1em 2em;
|
||||
}
|
||||
|
||||
.play-file {
|
||||
border: 1px solid green;
|
||||
}
|
||||
|
||||
.manual-controls {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.manual-controls .ui-button:not(:last-child) {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.output {
|
||||
overflow-y: auto;
|
||||
min-height: 70px;
|
||||
}
|
||||
|
||||
.field-name {
|
||||
background-color: #a5d6a770;
|
||||
}
|
||||
|
||||
.loc-time-v {
|
||||
display: flex;
|
||||
padding-bottom: 0.1em;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.loc-time {
|
||||
padding-top: .45em;
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
*:focus {
|
||||
outline: none;
|
||||
}
|
||||
@ -1,76 +0,0 @@
|
||||
<ng-container *ngIf="isCompLoaded(); else err">
|
||||
<div class="ui-g">
|
||||
<div class="ui-g-12 ui-md-11 ui-lg-10 ui-xl-8" style="margin: auto;;min-width: 564px">
|
||||
<div class="ui-g">
|
||||
<h1 style="margin-bottom: 1em;" i18n="@@pmtHist">Payment history</h1>
|
||||
<div class="ui-g ui-g-12">
|
||||
<div class="ui-g-12">
|
||||
<div class="ui-g">
|
||||
<div class="ui-g-8"><ng-container i18n="@@pmtHistMsg">If you recently made a payment, please allow 24 hours for the payment to appear in the history.</ng-container></div>
|
||||
<div class="ui-g-4" style="display: flex; justify-content: end;">
|
||||
<p-dropdown [options]="options" [(ngModel)]="optKey" (onChange)="onDateChange($event)"></p-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui-g-12">
|
||||
<p-table (sortFunction)="customSort($event)" [customSort]="true" [value]="payments" [columns]="cols" [paginator]="true" [responsive]="true" [rows]="10" [rowsPerPageOptions]="[5,10,20]" [sortField]="date" sortOrder="-1">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th [pSortableColumn]="col.field" class="pm-history-header" *ngFor="let col of cols" [width]="col.width">
|
||||
{{col.header}}
|
||||
<p-sortIcon [field]="col.field"></p-sortIcon>
|
||||
</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="body" let-rowData let-columns="columns">
|
||||
<tr>
|
||||
<td *ngFor="let col of columns" [ngSwitch]="col.field">
|
||||
<span class="ui-column-title">{{col.header}}</span>
|
||||
<span *ngSwitchCase="date">{{rowData[col.field] | tsToDate: lang}}</span>
|
||||
|
||||
<span *ngSwitchCase="TYPE">
|
||||
<span *ngIf="rowData.object === InvType.INVOICE" i18n="@@bill">Bill</span>
|
||||
<span *ngIf="rowData.object === InvType.CHARGE" i18n="@@refund">Refund</span>
|
||||
</span>
|
||||
|
||||
<span *ngSwitchCase="AMT_DUE">
|
||||
<ng-container *ngIf="rowData.object === InvType.INVOICE">
|
||||
{{rowData.amount_due | usCurrency}}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="rowData.object === InvType.CHARGE">
|
||||
{{rowData.amount_refunded | usCurrency | creditCurrency}}
|
||||
</ng-container>
|
||||
</span>
|
||||
|
||||
<span *ngSwitchCase="AMT_PAID">
|
||||
<ng-container *ngIf="rowData.object === InvType.INVOICE">
|
||||
{{rowData.amount_paid | usCurrency}}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="rowData.object === InvType.CHARGE">
|
||||
{{rowData.amount_refunded | usCurrency | creditCurrency}}
|
||||
</ng-container>
|
||||
</span>
|
||||
<span *ngSwitchCase="ACTIONS"><button (click)="gotoPaymentDetail(rowData)" pButton icon="ui-icon-zoom-in"></button></span>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #err>
|
||||
<div class="ui-g">
|
||||
<div class="ui-g-12 ui-md-8 ui-lg-6 ui-xl-4" style="margin: auto;">
|
||||
<div class="ui-g" style="padding: 1em;">
|
||||
<div class="ui-g-12 card in-card-pad row-space">
|
||||
<generic-message [icon]="'error'" [iconStyle]="'color: red;'" [messages]="[{text: status?.message || SubTexts.contactSupport, style: 'title sub-messages' }, { text: SubTexts.textBackSub }]" [buttons]="[{ label: SubTexts.labelBack}]" (backEvt)="gotoMySubs()">
|
||||
</generic-message>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
File diff suppressed because one or more lines are too long
@ -1,2 +0,0 @@
|
||||
DEBUG = agm:*,maintainer:*
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
DB_NAME=agmission
|
||||
DB_USR=agm
|
||||
DB_PWD=Agm2017_dev
|
||||
DB_HOSTS=127.0.0.1:27017
|
||||
DB_REPLSET=rs0
|
||||
|
||||
DEBUG=agm:*
|
||||
|
||||
QUEUE_PORT=5672
|
||||
QUEUE_HOST=localhost
|
||||
|
||||
QUEUE_USR=agm
|
||||
QUEUE_PWD=Ag@Rabbit2019
|
||||
QUEUE_NAME_JOBS=satloc_logs
|
||||
QUEUE_VHOST='/'
|
||||
QUEUE_HEARTBEAT=580
|
||||
|
||||
SATLOC_USERNME=vendor@agnav.com
|
||||
SATLOC_PASSWORD=a{AFA4aZ
|
||||
@ -1,61 +0,0 @@
|
||||
[2025-04-16T14:27:10.730Z] Error: Error
|
||||
Message: ENOENT: no such file or directory, open '/home/justin/VSCodeProjects/AgMission/branches/satloc-integration/satloc/job_files/5001.job'
|
||||
Stack: Error: ENOENT: no such file or directory, open '/home/justin/VSCodeProjects/AgMission/branches/satloc-integration/satloc/job_files/5001.job'
|
||||
at Object.openSync (node:fs:590:3)
|
||||
at Object.readFileSync (node:fs:458:35)
|
||||
at Object.convertJobFileToArea (/home/justin/VSCodeProjects/AgMission/branches/satloc-integration/satloc/satloc-util.js:211:37)
|
||||
at main (/home/justin/VSCodeProjects/AgMission/branches/satloc-integration/satloc/index.js:18:29)
|
||||
at /home/justin/VSCodeProjects/AgMission/branches/satloc-integration/satloc/index.js:139:9
|
||||
at Object.<anonymous> (/home/justin/VSCodeProjects/AgMission/branches/satloc-integration/satloc/index.js:140:3)
|
||||
at Module._compile (node:internal/modules/cjs/loader:1198:14)
|
||||
at Object.Module._extensions..js (node:internal/modules/cjs/loader:1252:10)
|
||||
at Module.load (node:internal/modules/cjs/loader:1076:32)
|
||||
at Function.Module._load (node:internal/modules/cjs/loader:911:12)
|
||||
[2025-04-16T14:27:18.061Z] Error: Error
|
||||
Message: ENOENT: no such file or directory, open '/home/justin/VSCodeProjects/AgMission/branches/satloc-integration/satloc/job_files/5001.job'
|
||||
Stack: Error: ENOENT: no such file or directory, open '/home/justin/VSCodeProjects/AgMission/branches/satloc-integration/satloc/job_files/5001.job'
|
||||
at Object.openSync (node:fs:590:3)
|
||||
at Object.readFileSync (node:fs:458:35)
|
||||
at Object.convertJobFileToArea (/home/justin/VSCodeProjects/AgMission/branches/satloc-integration/satloc/satloc-util.js:211:37)
|
||||
at main (/home/justin/VSCodeProjects/AgMission/branches/satloc-integration/satloc/index.js:18:29)
|
||||
at /home/justin/VSCodeProjects/AgMission/branches/satloc-integration/satloc/index.js:139:9
|
||||
at Object.<anonymous> (/home/justin/VSCodeProjects/AgMission/branches/satloc-integration/satloc/index.js:140:3)
|
||||
at Module._compile (node:internal/modules/cjs/loader:1198:14)
|
||||
at Object.Module._extensions..js (node:internal/modules/cjs/loader:1252:10)
|
||||
at Module.load (node:internal/modules/cjs/loader:1076:32)
|
||||
at Function.Module._load (node:internal/modules/cjs/loader:911:12)
|
||||
[2025-04-16T14:30:19.316Z] Error: TypeError
|
||||
Message: Cannot read properties of undefined (reading 'replace')
|
||||
Stack: TypeError: Cannot read properties of undefined (reading 'replace')
|
||||
at testSatlocApi (/home/justin/VSCodeProjects/AgMission/branches/satloc-integration/satloc/index.js:61:56)
|
||||
at processTicksAndRejections (node:internal/process/task_queues:96:5)
|
||||
at async main (/home/justin/VSCodeProjects/AgMission/branches/satloc-integration/satloc/index.js:21:5)
|
||||
at async /home/justin/VSCodeProjects/AgMission/branches/satloc-integration/satloc/index.js:140:3
|
||||
[2025-04-16T14:37:48.206Z] Error: TypeError
|
||||
Message: Cannot read properties of undefined (reading 'replace')
|
||||
Stack: TypeError: Cannot read properties of undefined (reading 'replace')
|
||||
at testSatlocApi (/home/justin/VSCodeProjects/AgMission/branches/satloc-integration/satloc/index.js:61:56)
|
||||
at processTicksAndRejections (node:internal/process/task_queues:96:5)
|
||||
at async main (/home/justin/VSCodeProjects/AgMission/branches/satloc-integration/satloc/index.js:21:5)
|
||||
at async /home/justin/VSCodeProjects/AgMission/branches/satloc-integration/satloc/index.js:140:3
|
||||
[2025-04-16T14:49:00.133Z] Error: TypeError
|
||||
Message: Cannot read properties of undefined (reading 'replace')
|
||||
Stack: TypeError: Cannot read properties of undefined (reading 'replace')
|
||||
at testSatlocApi (/home/justin/VSCodeProjects/AgMission/branches/satloc-integration/satloc/index.js:61:56)
|
||||
at processTicksAndRejections (node:internal/process/task_queues:96:5)
|
||||
at async main (/home/justin/VSCodeProjects/AgMission/branches/satloc-integration/satloc/index.js:21:5)
|
||||
at async /home/justin/VSCodeProjects/AgMission/branches/satloc-integration/satloc/index.js:140:3
|
||||
[2025-04-16T15:27:45.505Z] Error: ReferenceError
|
||||
Message: debug is not defined
|
||||
Stack: ReferenceError: debug is not defined
|
||||
at Object.writeSatlocLogData (/home/justin/VSCodeProjects/AgMission/branches/satloc-integration/satloc/satloc-util.js:448:3)
|
||||
at async testSatlocApi (/home/justin/VSCodeProjects/AgMission/branches/satloc-integration/satloc/index.js:65:5)
|
||||
at async main (/home/justin/VSCodeProjects/AgMission/branches/satloc-integration/satloc/index.js:21:5)
|
||||
at async /home/justin/VSCodeProjects/AgMission/branches/satloc-integration/satloc/index.js:140:3
|
||||
[2025-10-03T20:01:27.747Z] Error: TypeError
|
||||
Message: Cannot read properties of undefined (reading 'replace')
|
||||
Stack: TypeError: Cannot read properties of undefined (reading 'replace')
|
||||
at testSatlocApi (/home/trung/work/AgMission/branches/satloc-resume/satloc/index.js:61:56)
|
||||
at processTicksAndRejections (node:internal/process/task_queues:96:5)
|
||||
at async main (/home/trung/work/AgMission/branches/satloc-resume/satloc/index.js:21:5)
|
||||
at async /home/trung/work/AgMission/branches/satloc-resume/satloc/index.js:180:3
|
||||
Binary file not shown.
Binary file not shown.
@ -1 +0,0 @@
|
||||
engine-strict=true
|
||||
@ -1 +0,0 @@
|
||||
jobId,orderNumber,jobName,sessionId,fileName,pilotName,timestampUtc,gpsTime,lat,lon,utmX,utmY,alt_m,groundSpeed_ms,heading,crossTrackError_m,lockedLine,hdop,satsInView,correctionId,waasId,sprayStat,flowRateApplied_Lmin,flowRateRequired_Lmin,appRateRequired_Lha,appRateApplied_Lha,swathWidth_m,boomPressure_psi,sprayOnLag_s,sprayOffLag_s,pulsesPerLitre,windSpeed_ms,windDir_deg,temp_c,humidity_pct
|
||||
|
@ -1 +0,0 @@
|
||||
jobId,orderNumber,jobName,sessionId,fileName,pilotName,timestampUtc,gpsTime,lat,lon,utmX,utmY,alt_m,groundSpeed_ms,heading,crossTrackError_m,lockedLine,hdop,satsInView,correctionId,waasId,sprayStat,flowRateApplied_Lmin,flowRateRequired_Lmin,appRateRequired_Lha,appRateApplied_Lha,swathWidth_m,boomPressure_psi,sprayOnLag_s,sprayOffLag_s,pulsesPerLitre,windSpeed_ms,windDir_deg,temp_c,humidity_pct
|
||||
|
@ -1,56 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const puppeteer = require('puppeteer'),
|
||||
{ AppInputError } = require('./app_error'),
|
||||
debug = require('debug')('agm:web-util');
|
||||
|
||||
/**
|
||||
* Take screenshot of a screen using a headless webdriver
|
||||
* @param {*} params screenshot options { url: url, type: 'jpeg', 'png' (default), quality: 1-100 (75% default, jpeg only), width: number, height: number, path: path to save the output image }
|
||||
*/
|
||||
async function webShot(params, ops = { logTime: false, timeout: 30000 }) {
|
||||
const logTime = !!(ops && ops.logTime);
|
||||
if (logTime) console.time('webShot');
|
||||
const type = params.type || 'png';
|
||||
const quality = params.quality || 75;
|
||||
const width = params.width || 800;
|
||||
const height = params.height || 600;
|
||||
|
||||
if (!params || !params.url || !params.path) AppInputError.throw();
|
||||
|
||||
let browser;
|
||||
try {
|
||||
browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
// headless: false,
|
||||
slowMo: 250,
|
||||
args: ['--incognito'],
|
||||
ignoreHTTPSErrors: true,
|
||||
ignoreDefaultArgs: ['--disable-dev-shm-usage'],
|
||||
defaultViewport: { width: width, height: height },
|
||||
fullPage: true
|
||||
});
|
||||
|
||||
const pages = await browser.pages();
|
||||
const page = pages.length ? pages[0] : await browser.newPage();
|
||||
await page.goto(params.url);
|
||||
// const selector = 'div.gm-style-cc a';
|
||||
// await page.waitForFunction(selector => !!document.querySelector(selector), { timeout: 10000 }, selector);
|
||||
// Generic wait condition when tiles all finished loading, the page set loaded can add some delay to make sure they all loaded visually perfect
|
||||
await page.waitForFunction('window.loaded == true', { timeout: ops.timeout });
|
||||
|
||||
const shotOps = { type: type, clip: { x: 0, y: 0, width: width, height: height }, path: params.path };
|
||||
if (type == 'jpeg') shotOps['quality'] = quality;
|
||||
await page.screenshot(shotOps);
|
||||
} catch (err) {
|
||||
debug("input:", params);
|
||||
throw err;
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
if (logTime) console.timeEnd('webShot');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
webShot,
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
[
|
||||
{
|
||||
"username": "vcmosquito@volusia.org",
|
||||
"package": "ESS-2",
|
||||
"trackingQty": 3,
|
||||
"startDate": "26/03/2025",
|
||||
"endDate": "26/03/2026",
|
||||
"taxable": "N"
|
||||
}
|
||||
]
|
||||
@ -1,21 +0,0 @@
|
||||
|
||||
const MongoClient = require('mongodb').MongoClient
|
||||
|
||||
const PROD_URI = "mongodb://agm:Agm2017@localhost:27017/agmission";
|
||||
|
||||
async function connect(url) {
|
||||
let _url;
|
||||
if (!url) _url = PROD_URI;
|
||||
|
||||
return await MongoClient.connect(_url, {
|
||||
family: 4,
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
bufferMaxEntries: 0,
|
||||
keepAlive: true
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
connect
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
module.exports = {
|
||||
PORT: process.env.PORT,
|
||||
// SSE stream's heartbear time (secs). The interval that the server will send 'p' heartbeat to the browser to keep the connection alive
|
||||
HEARTBEAT: Number(process.env.HEARTBEAT),
|
||||
// SSE stream's reconnection time (secs). If the connection to the server is lost, the browser will wait for the specified time before attempting to reconnect
|
||||
RETRY_INT: Number(process.env.RETRY_INT),
|
||||
|
||||
SSL_KEY: process.env.SSL_KEY,
|
||||
SSL_CERT: process.env.SSL_CERT,
|
||||
|
||||
// DB connection info
|
||||
DB_HOSTS: process.env.DB_HOSTS,
|
||||
DB_NAME: process.env.DB_NAME,
|
||||
DB_USR: process.env.DB_USR,
|
||||
DB_PWD: process.env.DB_PWD,
|
||||
DB_REPLSET: process.env.DB_REPLSET,
|
||||
|
||||
// RabbitMq connection info
|
||||
QUEUE_PORT: process.env.QUEUE_PORT,
|
||||
QUEUE_HOST: process.env.QUEUE_HOST,
|
||||
QUEUE_USR: process.env.QUEUE_USR,
|
||||
QUEUE_PWD: process.env.QUEUE_PWD,
|
||||
QUEUE_VHOST: process.env.QUEUE_VHOST,
|
||||
QUEUE_NAME_GDATA: process.env.QUEUE_NAME_GDATA,
|
||||
QUEUE_HEARTBEAT: process.env.QUEUE_HEARTBEAT,
|
||||
|
||||
}
|
||||
@ -1,818 +0,0 @@
|
||||
# AgMission SaaS Platform Software Architecture
|
||||
|
||||
**Version:** 3.2.x
|
||||
**Last updated:** April 2026
|
||||
**Author:** AgNav Engineering
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [AgMission SaaS Platform Software Architecture](#agmission-saas-platform-software-architecture)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [1 Product Overview](#1-product-overview)
|
||||
- [2 System Components](#2-system-components)
|
||||
- [3 High-Level Architecture](#3-high-level-architecture)
|
||||
- [4 Component Details](#4-component-details)
|
||||
- [4-1 Web Client Angular SPA](#4-1-web-client-angular-spa)
|
||||
- [Module structure](#module-structure)
|
||||
- [State management NgRx](#state-management-ngrx)
|
||||
- [Key features](#key-features)
|
||||
- [4-2 API Server](#4-2-api-server)
|
||||
- [Request lifecycle](#request-lifecycle)
|
||||
- [Route groups](#route-groups)
|
||||
- [Key server-side helpers](#key-server-side-helpers)
|
||||
- [4-3 GPS Server](#4-3-gps-server)
|
||||
- [How it works](#how-it-works)
|
||||
- [4-4 Track Server](#4-4-track-server)
|
||||
- [4-5 SatLoc Integration Service](#4-5-satloc-integration-service)
|
||||
- [4-6 Background Workers](#4-6-background-workers)
|
||||
- [Worker overview](#worker-overview)
|
||||
- [Job Worker](#job-worker)
|
||||
- [Invoice Worker](#invoice-worker)
|
||||
- [Partner Sync Worker](#partner-sync-worker)
|
||||
- [Partner Data Polling Worker](#partner-data-polling-worker)
|
||||
- [Cleanup Worker](#cleanup-worker)
|
||||
- [4-7 Maintainer Service](#4-7-maintainer-service)
|
||||
- [5 Data Architecture](#5-data-architecture)
|
||||
- [MongoDB collections](#mongodb-collections)
|
||||
- [User type hierarchy](#user-type-hierarchy)
|
||||
- [6 Message Queue Architecture](#6-message-queue-architecture)
|
||||
- [7 Partner Integration Architecture](#7-partner-integration-architecture)
|
||||
- [Partner credential model](#partner-credential-model)
|
||||
- [8 Billing and Subscription Architecture](#8-billing-and-subscription-architecture)
|
||||
- [9 Authentication and Authorization](#9-authentication-and-authorization)
|
||||
- [JWT authentication](#jwt-authentication)
|
||||
- [API key authentication Public Export API](#api-key-authentication-public-export-api)
|
||||
- [Authorization model](#authorization-model)
|
||||
- [10 Infrastructure and Deployment](#10-infrastructure-and-deployment)
|
||||
- [Production server topology](#production-server-topology)
|
||||
- [PM2 managed processes](#pm2-managed-processes)
|
||||
- [Deployment flow](#deployment-flow)
|
||||
- [11 Key Data Flows](#11-key-data-flows)
|
||||
- [Job creation and assignment](#job-creation-and-assignment)
|
||||
- [Real-time GPS tracking](#real-time-gps-tracking)
|
||||
- [12 Repository Layout](#12-repository-layout)
|
||||
|
||||
---
|
||||
|
||||
## 1 Product Overview
|
||||
|
||||
AgMission is a cloud-based SaaS platform for **precision aerial agriculture** management. It serves agricultural aviation operators (applicators), their clients (growers/farmers), and integrated partners with console navigation aid systems (such as AgNav—Platinum and Titanium—and SatLoc).
|
||||
|
||||
The platform manages the complete lifecycle of an aerial application job:
|
||||
|
||||
1. **Mission planning** — create jobs, define treatment areas, assign pilots and jobs to aircraft
|
||||
2. **Job distribution** — push assigned jobs to guidance console systems, in aircraft, on the field
|
||||
3. **Real-time tracking** — live GPS monitoring of aircraft during operations
|
||||
4. **Post-flight processing** — parse binary GPS/spray logs from console systems, compute coverage statistics
|
||||
5. **Reporting and billing** — generate application reports, manage invoices, and handle SaaS subscriptions
|
||||
|
||||
**Supported languages:** English, Portuguese (pt), Spanish (es)
|
||||
|
||||
---
|
||||
|
||||
## 2 System Components
|
||||
|
||||
| Component | Technology | Purpose |
|
||||
|---|---|---|
|
||||
| Web Client | Angular 9.1.13, NgRx 9, Leaflet 1.9, StimulSoftJS 2020.3.2 | Browser-based management UI and report viewer |
|
||||
| API Server | Node.js 16.20.2 LTS, Express 4, Mongoose 6 | Core REST API, business logic |
|
||||
| GPS Server | Node.js, TCP sockets | Receives GPS data from AgNav (Platinum, Titanium) and RAP devices |
|
||||
| Track Server | Node.js, Express, SSE | Real-time GPS tracking feed to browser |
|
||||
| SatLoc Service | Node.js | Partner integration with SatLoc Cloud |
|
||||
| Job Worker | Node.js, RabbitMQ | Async processing of uploaded job files |
|
||||
| Invoice Worker | Node.js, Cron | Job Invoicing—manages applicator invoices for jobs performed |
|
||||
| Partner Sync Worker | Node.js, RabbitMQ | Partner job upload and log processing |
|
||||
| Partner Polling Worker | Node.js, Cron | Polls partner systems for new flight data |
|
||||
| Cleanup Worker | Node.js, Cron | Purges soft-deleted data |
|
||||
| Maintainer | Node.js, Cron | Scheduled database maintenance |
|
||||
|
||||
---
|
||||
|
||||
## 3 High-Level Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "Field Devices"
|
||||
AGN["AgNav Guidance<br/>System"]
|
||||
RAP["RAP Guidance<br/>System"]
|
||||
SLC["SatLoc Cloud<br/>Partner"]
|
||||
end
|
||||
|
||||
subgraph "Browser"
|
||||
UI["Angular SPA<br/>(Web Client)"]
|
||||
end
|
||||
|
||||
subgraph "AgMission Backend (agnav.com server)"
|
||||
NGINX["Nginx<br/>Reverse Proxy"]
|
||||
API["API Server<br/>Node.js / Express"]
|
||||
GPS["GPS Server<br/>TCP :6080 / :6082"]
|
||||
TRK["Track Server<br/>HTTP/SSE"]
|
||||
MNT["Maintainer<br/>Cron Service"]
|
||||
JW["Job Worker"]
|
||||
IW["Invoice Worker"]
|
||||
PSW["Partner Sync Worker"]
|
||||
PPW["Partner Polling Worker"]
|
||||
CW["Cleanup Worker"]
|
||||
end
|
||||
|
||||
subgraph "Data Layer"
|
||||
MDB["MongoDB<br/>Replica Set"]
|
||||
RBT["RabbitMQ<br/>Message Broker"]
|
||||
RDS["Redis<br/>Cache"]
|
||||
end
|
||||
|
||||
subgraph "External Services"
|
||||
STR["Stripe<br/>Billing"]
|
||||
SLC2["SatLoc Cloud API<br/>satloccloudfc.com"]
|
||||
SMTP["Email / SMTP"]
|
||||
end
|
||||
|
||||
AGN -- "TCP :6080" --> GPS
|
||||
RAP -- "TCP :6082" --> GPS
|
||||
UI -- "HTTPS" --> NGINX
|
||||
NGINX -- "/" --> API
|
||||
NGINX -- "/track" --> TRK
|
||||
GPS -- "AMQP gdata queue" --> RBT
|
||||
RBT -- "consume gdata" --> TRK
|
||||
TRK -- "SSE" --> UI
|
||||
API -- "AMQP jobs / partner_tasks" --> RBT
|
||||
RBT -- "consume jobs" --> JW
|
||||
RBT -- "consume partner_tasks" --> PSW
|
||||
API --> MDB
|
||||
JW --> MDB
|
||||
IW --> MDB
|
||||
PSW --> MDB
|
||||
PPW --> MDB
|
||||
CW --> MDB
|
||||
MNT --> MDB
|
||||
API --> RDS
|
||||
JW --> RDS
|
||||
API --> STR
|
||||
PSW -- "REST" --> SLC2
|
||||
PPW -- "REST / cron" --> SLC2
|
||||
PPW -- "AMQP partner_tasks" --> RBT
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4 Component Details
|
||||
|
||||
### 4-1 Web Client Angular SPA
|
||||
|
||||
**Location:** `trunk/Development/client/`
|
||||
**Version:** 2.6.15
|
||||
|
||||
The frontend is a single-page application built with Angular 9 and served by Nginx. It communicates with the API Server over HTTPS and receives real-time GPS updates from the Track Server via Server-Sent Events (SSE).
|
||||
|
||||
#### Module structure
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
ROOT["AppModule<br/>(app.module.ts)"]
|
||||
ROOT --> AUTH["AuthModule<br/>Login, signup, password reset"]
|
||||
ROOT --> DASH["DashboardModule<br/>Overview metrics"]
|
||||
ROOT --> JOBS["JobModule<br/>Mission management<br/>Map editing, file upload"]
|
||||
ROOT --> TRACK["TrackModule<br/>Live GPS tracking map"]
|
||||
ROOT --> CUST["CustomerModule<br/>Client / grower management"]
|
||||
ROOT --> BILL["BillingModule<br/>Subscription and invoices"]
|
||||
ROOT --> INV["InvoicesModule<br/>Invoice listing and detail"]
|
||||
ROOT --> PART["PartnersModule<br/>Partner system users"]
|
||||
ROOT --> ADM["AdminModule<br/>Platform admin tools"]
|
||||
ROOT --> SET["SettingsModule<br/>User and account settings"]
|
||||
ROOT --> REP["ReportComponent<br/>PDF report viewer"]
|
||||
ROOT --> SIGN["SignupModule<br/>Self-service subscription signup"]
|
||||
```
|
||||
|
||||
#### State management NgRx
|
||||
|
||||
The app uses NgRx (Redux pattern) for global state:
|
||||
|
||||
- **Store** — single source of truth for session, entities, and UI state
|
||||
- **Effects** — side-effects (HTTP calls) triggered by dispatched actions
|
||||
- **Entities** — normalized collections (jobs, customers, pilots, vehicles, etc.)
|
||||
- **Reducers** — pure state transitions
|
||||
|
||||
#### Key features
|
||||
|
||||
| Feature | Description |
|
||||
|---|---|
|
||||
| Job map editor | Leaflet map for defining treatment areas, waypoints, and obstacles |
|
||||
| Live tracking | Real-time aircraft position overlay via SSE |
|
||||
| Data playback | Replay completed flight paths from application logs |
|
||||
| Invoicing | Create, send, and track invoices |
|
||||
| Subscription management | Self-serve plan selection, trial, and upgrade; manage billing information; integrate with Stripe |
|
||||
| Multi-language | English / Portuguese / Spanish via Angular i18n |
|
||||
| Partner customers | View and manage SatLoc partner-linked customer accounts; support Satloc console systems, G4, Falcon in automated workflows |
|
||||
|
||||
---
|
||||
|
||||
### 4-2 API Server
|
||||
|
||||
**Location:** `trunk/Development/server/`
|
||||
**Entry point:** `server.js`
|
||||
**Port:** `AGM_PORT` (default 7000 in production)
|
||||
|
||||
The central Express application. All browser requests pass through Nginx which proxies to this server. It handles authentication, all business logic REST endpoints, file uploads, Stripe webhooks, and report generation.
|
||||
|
||||
#### Request lifecycle
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client (Browser)
|
||||
participant N as Nginx
|
||||
participant S as API Server
|
||||
participant MW as Middlewares
|
||||
participant R as Routes
|
||||
participant CTL as Controller
|
||||
participant DB as MongoDB
|
||||
|
||||
C->>N: HTTPS request
|
||||
N->>S: HTTP proxy (X-Forwarded-*)
|
||||
S->>MW: Rate limiter
|
||||
MW->>MW: JWT checkUser
|
||||
MW->>R: Router dispatch
|
||||
R->>CTL: Handler function
|
||||
CTL->>DB: Mongoose queries
|
||||
DB-->>CTL: Documents
|
||||
CTL-->>C: JSON response
|
||||
```
|
||||
|
||||
#### Route groups
|
||||
|
||||
| Route prefix | Controller | Description |
|
||||
|---|---|---|
|
||||
| `/api/users` | user.js | User account CRUD |
|
||||
| `/api/customers` | customer.js | Applicator / client management |
|
||||
| `/api/jobs` | job.js | Mission lifecycle |
|
||||
| `/api/upload` | upload_job.js | Job file upload (ZIP/KML/SHP) |
|
||||
| `/api/pilots` | pilot.js | Pilot management |
|
||||
| `/api/vehicles` | vehicle.js | Aircraft management |
|
||||
| `/api/billing` | billing.js | Stripe subscription billing |
|
||||
| `/api/subscription` | subscription.js | Subscription plans |
|
||||
| `/api/invoices` | invoice.js | Invoice management |
|
||||
| `/api/invoice_settings` | invoice_settings.js | Invoice templates |
|
||||
| `/api/partners` | partner.js | Partner org and system users |
|
||||
| `/api/dlq/:queue` | dlq.js | Dead Letter Queue management |
|
||||
| `/api/export` | export.js | Data export (CSV/IIF) |
|
||||
| `/api/v1` | api_pub.js | Public data export API (API-key auth) |
|
||||
| `/api/health` | health.js | Health check endpoint |
|
||||
| `/stripe_webhooks` | subscription_webhooks.js | Stripe event webhook |
|
||||
|
||||
#### Key server-side helpers
|
||||
|
||||
| Helper | Purpose |
|
||||
|---|---|
|
||||
| `helpers/constants.js` | Frozen enums (UserTypes, AppStatus, etc.) |
|
||||
| `helpers/env.js` | Typed environment variable access |
|
||||
| `helpers/subscription_util.js` | Stripe SDK wrappers |
|
||||
| `helpers/job_util.js` | Job state machine logic |
|
||||
| `helpers/geo_util.js` | Geospatial calculations (turf.js) |
|
||||
| `helpers/satloc_log_parser.js` | Binary SatLoc log file parser |
|
||||
| `helpers/satloc_application_processor.js` | Spray statistics from parsed logs |
|
||||
| `helpers/mailer.js` | Transactional email |
|
||||
| `helpers/logger.js` | Pino structured logging |
|
||||
|
||||
---
|
||||
|
||||
### 4-3 GPS Server
|
||||
|
||||
**Location:** `trunk/Development/gps-server/`
|
||||
**Entry point:** `gps-server.js`
|
||||
**Ports:** TCP 6080 (AgNav), TCP 6082 (RAP)
|
||||
|
||||
A low-level TCP socket server that receives binary GPS telemetry from field console navigation aid systems (AgNav, RAP) in real-time.
|
||||
|
||||
#### How it works
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant D as AgNav / RAP Console System
|
||||
participant G as GPS Server (TCP)
|
||||
participant MDB as MongoDB
|
||||
participant RBT as RabbitMQ
|
||||
|
||||
D->>G: TCP binary packet
|
||||
G->>G: Parse (AgNavParser / RAPParser)
|
||||
G->>MDB: Upsert location + location_cache
|
||||
G->>RBT: Publish to "gdata" queue
|
||||
```
|
||||
|
||||
Two protocol variants are configured via `PROTOCOL` env var:
|
||||
|
||||
| Protocol | Port | Device type |
|
||||
|---|---|---|
|
||||
| `AGNAV` | 6080 | AgNav console system (Platinum, Titanium) |
|
||||
| `RAP` | 6082 | RAP binary protocol (external tracking devices) |
|
||||
|
||||
The GPS Server writes every position to MongoDB (`locations` + `location_cache` collections) and simultaneously publishes to a RabbitMQ queue `gdata`, which the Track Server consumes for live SSE streaming.
|
||||
|
||||
---
|
||||
|
||||
### 4-4 Track Server
|
||||
|
||||
**Location:** `trunk/Development/track-server/`
|
||||
**Entry point:** `track-server.js`
|
||||
**Protocol:** HTTP/2 (via `spdy`) + Server-Sent Events
|
||||
|
||||
The Track Server bridges the real-time GPS queue to browser clients using SSE channels. Each client subscribes to one or more vehicle channels; the server pushes position updates as they arrive from RabbitMQ.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant RBT as RabbitMQ gdata
|
||||
participant TS as Track Server
|
||||
participant BR as Browser (SSE client)
|
||||
|
||||
RBT-->>TS: GPS data message
|
||||
TS->>TS: setVehGpsData(data)
|
||||
TS->>BR: SSE event (vehicle position)
|
||||
```
|
||||
|
||||
Authentication is JWT-based — clients obtain a short-lived track token from the API Server, then connect to Track Server with it.
|
||||
|
||||
---
|
||||
|
||||
### 4-5 SatLoc Integration Service
|
||||
|
||||
**Location:** `trunk/Development/satloc/`
|
||||
**Purpose:** Batch job of importing completed flight logs from the SatLoc Cloud partner system.
|
||||
|
||||
SatLoc is an aerial guidance hardware vendor. When an applicator uses SatLoc hardware, their flight logs are stored in SatLoc's cloud. This service:
|
||||
|
||||
1. Authenticates with SatLoc Cloud (`satloccloudfc.com`)
|
||||
2. Retrieves aircraft log metadata
|
||||
3. Downloads binary log files
|
||||
4. Parses the proprietary binary format
|
||||
5. Creates `ApplicationDetail` records in AgMission's database
|
||||
|
||||
The core C# parsing logic (`frmMain.cs`, `APIObjects.cs`) is the original reference implementation; the production path is the Node.js port in `satloc-api.js` and the server-side `helpers/satloc_log_parser.js`.
|
||||
|
||||
---
|
||||
|
||||
### 4-6 Background Workers
|
||||
|
||||
All workers are independent Node.js processes managed by PM2. They communicate via RabbitMQ queues and share the same MongoDB instance as the API Server.
|
||||
|
||||
#### Worker overview
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
API["API Server"] -- "publish jobs" --> JQ["jobs queue<br/>(RabbitMQ)"]
|
||||
API -- "publish partner_tasks" --> PQ["partner_tasks queue<br/>(RabbitMQ)"]
|
||||
JQ --> JW["Job Worker<br/>job_worker.js"]
|
||||
PQ --> PSW["Partner Sync Worker<br/>partner_sync_worker.js"]
|
||||
PPW["Partner Polling Worker<br/>(cron every 15 min)"] -- "publish partner_tasks" --> PQ
|
||||
PPW -- "REST poll" --> SLCAPI["SatLoc Cloud API"]
|
||||
IW["Invoice Worker<br/>(cron every 1 min)"] --> MDB["MongoDB"]
|
||||
CW["Cleanup Worker<br/>(cron weekly)"] --> MDB
|
||||
JW --> MDB
|
||||
PSW --> MDB
|
||||
PSW -- "REST" --> SLCAPI
|
||||
```
|
||||
|
||||
#### Job Worker
|
||||
|
||||
Consumes the `jobs` RabbitMQ queue. Triggered when a user uploads a job file via the web UI.
|
||||
|
||||
Responsibilities:
|
||||
- Extract ZIP archives containing job data files
|
||||
- Parse AgNav binary (`.agn`), KML, and Shapefile formats
|
||||
- Calculate sprayed area, coverage geometry, application statistics
|
||||
- Create `Application`, `ApplicationFile`, and `ApplicationDetail` records
|
||||
- Use Redis for deduplication and temporary state
|
||||
|
||||
#### Invoice Worker
|
||||
|
||||
Cron-driven (every minute). Manages Job Invoicing—the lifecycle of applicator invoices for jobs performed on behalf of clients. Handles invoice state transitions and late-payment notifications.
|
||||
|
||||
**Note:** Job Invoicing is separate from SaaS subscription management (which is handled by Stripe webhooks in the API Server).
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
direction LR
|
||||
[*] --> Draft : invoice created
|
||||
Draft --> Open : openDate reached
|
||||
Open --> Overdue : dueDate passed
|
||||
Open --> Paid : payment received
|
||||
Overdue --> Paid : late payment
|
||||
Paid --> [*]
|
||||
```
|
||||
|
||||
#### Partner Sync Worker
|
||||
|
||||
Consumes the `partner_tasks` queue. Handles two task types:
|
||||
|
||||
| Task type | Action |
|
||||
|---|---|
|
||||
| `UPLOAD_PARTNER_JOB` | Pushes a job (waypoints, boundaries) to SatLoc Cloud for the assigned aircraft |
|
||||
| `PROCESS_PARTNER_LOG` | Parses a downloaded SatLoc binary log file and creates `ApplicationDetail` records |
|
||||
|
||||
#### Partner Data Polling Worker
|
||||
|
||||
Cron-driven (every 15 min in production, every 1 min in development).
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["Cron trigger"] --> B["Find JobAssigns<br/>status = UPLOADED"]
|
||||
B --> C["Group by partner + customer"]
|
||||
C --> D["Call SatLoc: GetAircraftLogs"]
|
||||
D --> E{"New log<br/>files?"}
|
||||
E -- No --> F["Done"]
|
||||
E -- Yes --> G["Download log file<br/>to local storage"]
|
||||
G --> H["Create PartnerLogTracker<br/>PENDING → DOWNLOADED"]
|
||||
H --> I["Enqueue PROCESS_PARTNER_LOG<br/>to partner_tasks queue"]
|
||||
```
|
||||
|
||||
#### Cleanup Worker
|
||||
|
||||
Weekly cron job. Hard-deletes records that have been soft-deleted (`markedDelete: true`) and older than a retention period. Applies to customers, jobs, pilots, vehicles, and related entities.
|
||||
|
||||
---
|
||||
|
||||
### 4-7 Maintainer Service
|
||||
|
||||
**Location:** `trunk/Development/maintainer/`
|
||||
**Entry point:** `index.js`
|
||||
|
||||
A lightweight cron-based utility for database maintenance tasks not suitable for the main server. Scheduled tasks:
|
||||
|
||||
| Task | Schedule (prod) | Description |
|
||||
|---|---|---|
|
||||
| `cleanMarkedDeleteData` | Weekly (Sunday 01:00 UTC) | Remove soft-deleted customer records |
|
||||
|
||||
Connects to MongoDB using the same models as the API Server (shared `model/` layer).
|
||||
|
||||
---
|
||||
|
||||
## 5 Data Architecture
|
||||
|
||||
### MongoDB collections
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
USER {
|
||||
ObjectId _id
|
||||
string kind
|
||||
string email
|
||||
string name
|
||||
string userType
|
||||
boolean active
|
||||
ObjectId parent
|
||||
}
|
||||
CUSTOMER {
|
||||
ObjectId _id
|
||||
string name
|
||||
ObjectId byPuid
|
||||
}
|
||||
JOB {
|
||||
number _id
|
||||
string name
|
||||
ObjectId customer
|
||||
ObjectId byPuid
|
||||
number status
|
||||
Date startDate
|
||||
}
|
||||
JOB_ASSIGN {
|
||||
ObjectId _id
|
||||
number job
|
||||
ObjectId user
|
||||
number status
|
||||
string extJobId
|
||||
}
|
||||
APPLICATION {
|
||||
ObjectId _id
|
||||
number jobId
|
||||
string fileName
|
||||
Date startDateTime
|
||||
Date endDateTime
|
||||
number totalSprayed
|
||||
}
|
||||
APPLICATION_DETAIL {
|
||||
ObjectId _id
|
||||
ObjectId appId
|
||||
number lat
|
||||
number lon
|
||||
number rate
|
||||
Date gdt
|
||||
}
|
||||
SUBSCRIPTION {
|
||||
ObjectId _id
|
||||
ObjectId byPuid
|
||||
string stripeSubId
|
||||
string planKey
|
||||
string status
|
||||
}
|
||||
INVOICE {
|
||||
ObjectId _id
|
||||
ObjectId byPuid
|
||||
number status
|
||||
Date openDate
|
||||
Date dueDate
|
||||
}
|
||||
PARTNER_LOG_TRACKER {
|
||||
ObjectId _id
|
||||
string status
|
||||
ObjectId jobAssignId
|
||||
string localFilePath
|
||||
}
|
||||
|
||||
USER ||--o{ JOB : "creates (byPuid)"
|
||||
CUSTOMER ||--o{ JOB : "associated"
|
||||
JOB ||--o{ JOB_ASSIGN : "assigned to"
|
||||
JOB_ASSIGN }o--|| USER : "pilot/device"
|
||||
JOB ||--o{ APPLICATION : "contains"
|
||||
APPLICATION ||--o{ APPLICATION_DETAIL : "detail records"
|
||||
USER ||--o{ SUBSCRIPTION : "holds"
|
||||
USER ||--o{ INVOICE : "receives"
|
||||
JOB_ASSIGN ||--o{ PARTNER_LOG_TRACKER : "tracks"
|
||||
```
|
||||
|
||||
### User type hierarchy
|
||||
|
||||
The `User` model uses a Mongoose discriminator pattern to represent multiple actor types from one collection:
|
||||
|
||||
| `userType` code | Kind | Description |
|
||||
|---|---|---|
|
||||
| `0` | ADMIN | Platform administrator |
|
||||
| `1` | APP | Applicator (main operator account) |
|
||||
| `2` | APP_ADM | Applicator admin |
|
||||
| `3` | CLIENT | Client / grower (read-only) |
|
||||
| `4` | OFFICER | Field officer |
|
||||
| `5` | PILOT | Pilot |
|
||||
| `6` | INSPECTOR | Inspector |
|
||||
| `9` | DEVICE | Aircraft / guidance unit |
|
||||
| `20` | PARTNER | Partner organization (e.g., SatLoc) |
|
||||
| `21` | PARTNER_SYSTEM_USER | Customer account in partner system |
|
||||
|
||||
---
|
||||
|
||||
## 6 Message Queue Architecture
|
||||
|
||||
RabbitMQ is used for all async work. Queue names are auto-prefixed with `dev_` in non-production environments.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph "Producers"
|
||||
API["API Server"]
|
||||
PPW["Partner Polling Worker"]
|
||||
GPS["GPS Server"]
|
||||
end
|
||||
|
||||
subgraph "Queues (RabbitMQ)"
|
||||
JQ["jobs"]
|
||||
PQ["partner_tasks"]
|
||||
PDLQ["partner_tasks_failed<br/>(DLQ)"]
|
||||
GQ["gdata"]
|
||||
end
|
||||
|
||||
subgraph "Consumers"
|
||||
JW["Job Worker"]
|
||||
PSW["Partner Sync Worker"]
|
||||
TRK["Track Server"]
|
||||
end
|
||||
|
||||
API --> JQ
|
||||
API --> PQ
|
||||
PPW --> PQ
|
||||
GPS --> GQ
|
||||
JQ --> JW
|
||||
PQ --> PSW
|
||||
PSW -- "on max retries" --> PDLQ
|
||||
GQ --> TRK
|
||||
```
|
||||
|
||||
The Dead Letter Queue (`partner_tasks_failed`) is managed through the `/api/dlq/:queueName/*` API endpoints, which provide list, retry, and purge operations.
|
||||
|
||||
---
|
||||
|
||||
## 7 Partner Integration Architecture
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant UI as Web Client
|
||||
participant API as API Server
|
||||
participant PSW as Partner Sync Worker
|
||||
participant PPW as Partner Polling Worker
|
||||
participant SLC as SatLoc Cloud
|
||||
|
||||
UI->>API: Assign job to SatLoc device
|
||||
API->>API: Create JobAssign (status=NEW)
|
||||
API->>PSW: Enqueue UPLOAD_PARTNER_JOB
|
||||
PSW->>SLC: POST /api/Satloc/UploadJobData
|
||||
SLC-->>PSW: extJobId
|
||||
PSW->>API: Update JobAssign (status=UPLOADED, extJobId)
|
||||
|
||||
Note over PPW: Cron: every 15 min
|
||||
PPW->>API: Find JobAssigns status=UPLOADED
|
||||
PPW->>SLC: GET /api/Satloc/GetAircraftLogs
|
||||
SLC-->>PPW: Log list
|
||||
PPW->>SLC: GET /api/Satloc/GetAircraftLogData
|
||||
SLC-->>PPW: Binary log file
|
||||
PPW->>PPW: Store file locally (SATLOC_STORAGE_PATH)
|
||||
PPW->>API: Create PartnerLogTracker (DOWNLOADED)
|
||||
PPW->>PSW: Enqueue PROCESS_PARTNER_LOG
|
||||
PSW->>PSW: Parse binary log (SatLocLogParser)
|
||||
PSW->>API: Create ApplicationDetail records
|
||||
```
|
||||
|
||||
### Partner credential model
|
||||
|
||||
Each customer that uses a SatLoc device has a dedicated `PartnerSystemUser` record (userType=21) that stores their SatLoc `companyId`, `partnerUserId`, and API key. This isolates customer data within the partner system.
|
||||
|
||||
---
|
||||
|
||||
## 8 Billing and Subscription Architecture
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant UI as Web Client
|
||||
participant API as API Server
|
||||
participant STR as Stripe
|
||||
|
||||
UI->>API: Select subscription plan
|
||||
API->>STR: Create SetupIntent or Subscription
|
||||
STR-->>API: Client secret
|
||||
UI->>STR: Confirm card (3DS if needed)
|
||||
STR->>API: POST /stripe_webhooks (subscription events)
|
||||
API->>API: Update Subscription record
|
||||
```
|
||||
|
||||
Note: SaaS subscription management (tiers, billing cycles) is handled through Stripe webhooks. Job Invoicing (applicators invoicing clients for jobs performed) is a separate function managed by the Invoice Worker.
|
||||
|
||||
**SaaS Subscription tiers** (mapped to Stripe price IDs via env vars):
|
||||
|
||||
| Tier | Env key prefix | Description |
|
||||
|---|---|---|
|
||||
| Essential | `ESS_1` … `ESS_5` | Entry-level operator plans |
|
||||
| Enterprise | `ENT_1` … `ENT_4` | High-volume operator plans |
|
||||
| Add-on | `ADDON_1` | Additional features, refer to Live Tracking service |
|
||||
|
||||
---
|
||||
|
||||
## 9 Authentication and Authorization
|
||||
|
||||
### JWT authentication
|
||||
|
||||
All API Server endpoints (except signup and Stripe webhooks) require a JWT Bearer token:
|
||||
|
||||
```
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
Tokens are issued on login and carry `userType`, `byPuid` (applicator ID), and `userId`. The `checkUser` middleware validates the token and attaches the user to `req.user`.
|
||||
|
||||
### API key authentication Public Export API
|
||||
|
||||
The `/api/v1/` public export endpoints use an `X-API-Key` header instead of JWT. Keys are bcrypt-hashed and stored in the `ApiKey` collection. Each key is scoped to a specific applicator (`byPuid`), so data access is automatically isolated.
|
||||
|
||||
### Authorization model
|
||||
|
||||
```
|
||||
ADMIN — full platform access
|
||||
APP — manages own organization (pilots, vehicles, jobs, customers)
|
||||
APP_ADM — same as APP within parent organization
|
||||
CLIENT — read-only access to own job results
|
||||
PILOT — limited: download job assignments
|
||||
OFFICER — field oversight
|
||||
INSPECTOR — read-only job inspection
|
||||
PARTNER — manages partner organization
|
||||
PARTNER_SYSTEM_USER — customer credentials for a specific partner
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10 Infrastructure and Deployment
|
||||
|
||||
### Production server topology
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
INT["Internet"] --> NX["Nginx<br/>SSL termination<br/>port 443"]
|
||||
NX -- "/ → :7000" --> API["agmission-prod<br/>(PM2)"]
|
||||
NX -- "/track → :4200" --> TRK["track_server<br/>(PM2)"]
|
||||
API --> MDB["MongoDB<br/>Replica Set<br/>rs0"]
|
||||
API --> RBT["RabbitMQ"]
|
||||
API --> RDS["Redis"]
|
||||
GPS1["gps_server-agnav<br/>TCP :6080 (PM2)"] --> MDB
|
||||
GPS1 --> RBT
|
||||
GPS2["gps_server-rap<br/>TCP :6082 (PM2)"] --> MDB
|
||||
GPS2 --> RBT
|
||||
RBT --> JW["job_worker<br/>(PM2)"]
|
||||
RBT --> PSW["partner_sync_worker<br/>(PM2)"]
|
||||
PPW["partner_data_polling_worker<br/>(PM2)"] --> MDB
|
||||
PPW --> RBT
|
||||
IW["invoice_worker<br/>(PM2)"] --> MDB
|
||||
CW["cleanup_worker<br/>(PM2)"] --> MDB
|
||||
```
|
||||
|
||||
### PM2 managed processes
|
||||
|
||||
| PM2 name | Entry point | Description |
|
||||
|---|---|---|
|
||||
| `agmission-prod` | `server.js` | Main API server |
|
||||
| `track_server` | `track-server.js` | Live tracking |
|
||||
| `gps_server-agnav` | `gps-server.js` | AgNav TCP receiver |
|
||||
| `gps_server-rap` | `gps-server.js` | RAP TCP receiver |
|
||||
| `job_worker` / `job-importer` | `workers/job_worker.js` | Job file processing |
|
||||
| `invoice_worker` | `workers/invoice_worker.js` | Invoice automation |
|
||||
| `cleanup_worker` | `workers/cleanup_worker.js` | Soft-delete cleanup |
|
||||
| `partner_sync_worker` | `workers/partner_sync_worker.js` | Partner job/log sync |
|
||||
| `partner_data_polling_worker` | `workers/partner_data_polling_worker.js` | Partner log polling |
|
||||
|
||||
### Deployment flow
|
||||
|
||||
Deployments are performed using `trunk/Others/scripts/deploy/agm-deploy.sh`. See [DEPLOYMENT.md](DEPLOYMENT.md) for full instructions.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
DEV["Local dev machine<br/>(SVN trunk or branch)"] -- "rsync over SSH<br/>agm-deploy.sh" --> PROD["Production server<br/>agmission-1.agnav.com:22222"]
|
||||
PROD --> PM2["pm2 reload<br/>agmission-prod"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11 Key Data Flows
|
||||
|
||||
### Job creation and assignment
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["Applicator creates job<br/>in Web UI"] --> B["POST /api/jobs"]
|
||||
B --> C["Job record created<br/>in MongoDB"]
|
||||
C --> D["Upload job file<br/>(ZIP / KML / SHP)"]
|
||||
D --> E["POST /api/upload"]
|
||||
E --> F["File stored on disk<br/>job-uploads/"]
|
||||
F --> G["Job message published<br/>to 'jobs' queue"]
|
||||
G --> H["Job Worker consumes<br/>message"]
|
||||
H --> I["Unzip and parse files"]
|
||||
I --> J["Calculate spray statistics"]
|
||||
J --> K["Create Application +<br/>ApplicationDetail records"]
|
||||
K --> L["Applicator assigns job<br/>to pilot / device / partner"]
|
||||
L --> M{"Partner?"}
|
||||
M -- "No (internal)" --> N["JobAssign created<br/>status=NEW"]
|
||||
M -- "Yes (SatLoc)" --> O["UPLOAD_PARTNER_JOB<br/>enqueued"]
|
||||
O --> P["Partner Sync Worker<br/>uploads to SatLoc Cloud"]
|
||||
P --> Q["JobAssign status=UPLOADED<br/>+ extJobId stored"]
|
||||
```
|
||||
|
||||
### Real-time GPS tracking
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
HW["AgNav Device"] -- "binary TCP" --> GPS["GPS Server"]
|
||||
GPS --> MDB["MongoDB<br/>locations"]
|
||||
GPS --> RBT["RabbitMQ gdata"]
|
||||
RBT --> TRK["Track Server"]
|
||||
TRK -- "SSE push" --> UI["Browser Map"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12 Repository Layout
|
||||
|
||||
```
|
||||
AgMission/
|
||||
├── trunk/
|
||||
│ ├── Development/ ← Active source code (see below)
|
||||
│ ├── Documents/ ← Architecture, requirements, design docs
|
||||
│ └── Others/
|
||||
│ ├── configs/ ← PM2 JSON configs for deployment target
|
||||
│ └── scripts/ ← Operations scripts
|
||||
│ ├── deploy/ ← Deployment automation (agm-deploy.sh)
|
||||
│ ├── backup_agm.sh ← MongoDB backup + rsync to NAS
|
||||
│ └── start_pm2_apps.sh
|
||||
│
|
||||
├── branches/ ← Feature branches (SVN layout)
|
||||
│ ├── subscription-invoicing/
|
||||
│ ├── subscription-signup/
|
||||
│ ├── job-invoicing/
|
||||
│ ├── data-export-api/
|
||||
│ └── satloc-resume/
|
||||
│
|
||||
└── tags/ ← Release snapshots
|
||||
└── release-3.2.1/
|
||||
|
||||
trunk/Development/
|
||||
├── client/ Angular SPA
|
||||
├── server/ Express API server + workers
|
||||
│ ├── controllers/
|
||||
│ ├── helpers/
|
||||
│ ├── middlewares/
|
||||
│ ├── model/
|
||||
│ ├── routes/
|
||||
│ ├── services/
|
||||
│ └── workers/
|
||||
├── gps-server/ TCP GPS receiver
|
||||
├── track-server/ SSE live tracking
|
||||
├── satloc/ SatLoc integration (batch import)
|
||||
├── maintainer/ Scheduled DB maintenance
|
||||
├── shared/ Shared DB utilities and models
|
||||
└── libs/ Bundled third-party libs (shapefile, etc.)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*For deployment instructions see [DEPLOYMENT.md](DEPLOYMENT.md).*
|
||||
*For the server-side REST API reference see [`server/docs/API_SPECIFICATION.md`](../Development/server/docs/API_SPECIFICATION.md).*
|
||||
*For partner integration details see [`server/docs/PARTNER_INTEGRATION_ARCHITECTURE.md`](../Development/server/docs/PARTNER_INTEGRATION_ARCHITECTURE.md).*
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,372 +0,0 @@
|
||||
# AgMission Deployment Guide
|
||||
|
||||
**Last updated:** April 2026
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [1 Prerequisites](#1-prerequisites)
|
||||
- [2 Repository and Directory Layout](#2-repository-and-directory-layout)
|
||||
- [3 Deployment Script Reference](#3-deployment-script-reference)
|
||||
- [4 Configuration](#4-configuration)
|
||||
- [5 Deployment Modes](#5-deployment-modes)
|
||||
- [6 Step-by-Step Production Deployment](#6-step-by-step-production-deployment)
|
||||
- [7 PM2 Process Management](#7-pm2-process-management)
|
||||
- [8 Backup and Recovery](#8-backup-and-recovery)
|
||||
- [9 Environment Variables Reference](#9-environment-variables-reference)
|
||||
|
||||
---
|
||||
|
||||
## 1 Prerequisites
|
||||
|
||||
**Developer machine (deploying from):**
|
||||
|
||||
- SSH access to the production server on port `22222`
|
||||
- `rsync` installed
|
||||
- SVN working copy at `~/work/AgMission`
|
||||
- AGN private libraries at `~/work/@agn`
|
||||
- Angular CLI for frontend builds (`npm install -g @angular/cli`)
|
||||
|
||||
**Production server:**
|
||||
|
||||
- Node.js 16.20.2 LTS
|
||||
- PM2 (global): `npm install -g pm2`
|
||||
- MongoDB 4.4.x replica set (`rs0`)
|
||||
- RabbitMQ 3.10.7+
|
||||
- Redis
|
||||
- Nginx 1.10.x+
|
||||
|
||||
---
|
||||
|
||||
## 2 Repository and Directory Layout
|
||||
|
||||
### Local developer machine
|
||||
|
||||
```
|
||||
~/work/AgMission/
|
||||
├── trunk/Development/ ← main line source
|
||||
├── branches/<branch-name>/ ← feature branches
|
||||
└── tags/release-x.y.z/ ← release snapshots
|
||||
|
||||
~/work/@agn/ ← private AGN Node.js libraries
|
||||
```
|
||||
|
||||
### Remote production server
|
||||
|
||||
```
|
||||
/home/agm/apps/
|
||||
├── agmission/ ← API server (server/)
|
||||
├── client/dist/ ← Angular build output
|
||||
├── gps-server/
|
||||
├── track-server/
|
||||
├── satloc/
|
||||
├── maintainer/
|
||||
├── pm2-apps/ ← PM2 JSON configs
|
||||
└── @agn/ ← private libs (rsync'd from ~/work/@agn)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3 Deployment Script Reference
|
||||
|
||||
**Script:** `trunk/Others/scripts/deploy/agm-deploy.sh`
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
./agm-deploy.sh [run_mode] [branch_name] [fe_mode]
|
||||
```
|
||||
|
||||
| Argument | Values | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `run_mode` | `0`, `1`, `2` | `0` | Deployment scope (see table below) |
|
||||
| `branch_name` | `trunk`, `main`, or branch name | `subscription-invoicing` | Source to deploy from |
|
||||
| `fe_mode` | `run`, `dry-run` | `run` | Frontend deploy behaviour (mode 2 only) |
|
||||
|
||||
| `run_mode` | Backend | Frontend | Use case |
|
||||
|---|---|---|---|
|
||||
| `0` (or empty) | Dry run | Dry run | Verify what would be synced |
|
||||
| `1` | Deploy | Skipped | Backend-only update |
|
||||
| `2` | Deploy | Deploy | Full production deployment |
|
||||
|
||||
### Configuration file recommended
|
||||
|
||||
```bash
|
||||
# 1. Copy template
|
||||
cp trunk/Others/scripts/deploy/agm-deploy.conf.template ~/.agm-deploy.conf
|
||||
|
||||
# 2. Edit values
|
||||
nano ~/.agm-deploy.conf
|
||||
|
||||
# 3. Use it
|
||||
source ~/.agm-deploy.conf && ./agm-deploy.sh 1 trunk
|
||||
```
|
||||
|
||||
### Environment variable overrides
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `AGM_BASE_DIR` | `~/work/AgMission` | Local AgMission SVN root |
|
||||
| `AGN_LIBS_DIR` | `~/work/@agn` | Local AGN private libraries |
|
||||
| `AGM_DEST_HOST` | `agm@agmission-1.agnav.com` | SSH target (`user@host`) |
|
||||
| `AGM_DEST_PORT` | `22222` | SSH port |
|
||||
| `AGM_DEST_PATH` | `/home/agm/apps` | Remote base directory |
|
||||
|
||||
---
|
||||
|
||||
## 4 Configuration
|
||||
|
||||
### Excluded files
|
||||
|
||||
`trunk/Others/scripts/deploy/excludes.txt` lists paths excluded from `rsync`. These typically include:
|
||||
|
||||
- `node_modules/`
|
||||
- `.env` / `environment*.env` (never overwrite production secrets)
|
||||
- `.tmp/`, `job-unzip/`, `job-uploads/`
|
||||
- Log files and rlog crash reports
|
||||
|
||||
### Production environment files
|
||||
|
||||
Environment files are **not** deployed by the script. They must be maintained manually on the production server. See [9 Environment Variables Reference](#9-environment-variables-reference) for key variables.
|
||||
|
||||
---
|
||||
|
||||
## 5 Deployment Modes
|
||||
|
||||
### Dry run mode 0
|
||||
|
||||
```bash
|
||||
./agm-deploy.sh 0 trunk
|
||||
```
|
||||
|
||||
Shows all files that would be synced without touching the server. Always run this first when deploying after a significant code change.
|
||||
|
||||
### Backend only mode 1
|
||||
|
||||
```bash
|
||||
./agm-deploy.sh 1 trunk
|
||||
```
|
||||
|
||||
Deploys:
|
||||
- `server/` → `/home/agm/apps/agmission/`
|
||||
- `gps-server/` → `/home/agm/apps/gps-server/`
|
||||
- `track-server/` → `/home/agm/apps/track-server/`
|
||||
- `shared/` → `/home/agm/apps/shared/`
|
||||
- `@agn` libraries → `/home/agm/apps/@agn/`
|
||||
|
||||
After sync, run `pm2 reload` on the server (see [7 PM2 Process Management](#7-pm2-process-management)).
|
||||
|
||||
### Full deployment mode 2
|
||||
|
||||
```bash
|
||||
./agm-deploy.sh 2 trunk
|
||||
```
|
||||
|
||||
Deploys everything in mode 1, plus the Angular frontend `client/dist/` to the Nginx webroot.
|
||||
|
||||
**Before running mode 2**, build the frontend locally:
|
||||
|
||||
```bash
|
||||
cd trunk/Development/client
|
||||
npm install
|
||||
npm run build-prod # builds all locales: en, pt, es
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6 Step-by-Step Production Deployment
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["1. Update SVN working copy<br/>svn update trunk/"] --> B["2. Run tests<br/>cd server && npm test"]
|
||||
B --> C["3. Build frontend<br/>cd client && npm run build-prod"]
|
||||
C --> D["4. Dry run<br/>./agm-deploy.sh 0 trunk"]
|
||||
D --> E{"Review OK?"}
|
||||
E -- No --> F["Fix issues"]
|
||||
F --> D
|
||||
E -- Yes --> G["5. Deploy backend<br/>./agm-deploy.sh 1 trunk"]
|
||||
G --> H["6. SSH to server<br/>ssh agm@agmission-1.agnav.com -p 22222"]
|
||||
H --> I["7. Install dependencies<br/>cd /home/agm/apps/agmission && npm install --production"]
|
||||
I --> J["8. Reload PM2<br/>pm2 reload agmission-prod"]
|
||||
J --> K["9. Check logs<br/>pm2 logs agmission-prod --lines 50"]
|
||||
K --> L{"Errors?"}
|
||||
L -- Yes --> M["Rollback: pm2 reload with previous snapshot"]
|
||||
L -- No --> N["10. Deploy frontend (if needed)<br/>./agm-deploy.sh 2 trunk"]
|
||||
```
|
||||
|
||||
### Post-deployment checklist
|
||||
|
||||
- [ ] `pm2 status` — all processes show `online`
|
||||
- [ ] `pm2 logs agmission-prod --lines 20` — no crash errors
|
||||
- [ ] Health check endpoint: `curl https://agmission-1.agnav.com/api/health`
|
||||
- [ ] Login to the web UI and confirm dashboard loads
|
||||
- [ ] If partner workers were updated: `pm2 reload partner_sync_worker partner_data_polling_worker`
|
||||
- [ ] If worker code changed: check `pm2 logs job_worker --lines 20`
|
||||
|
||||
---
|
||||
|
||||
## 7 PM2 Process Management
|
||||
|
||||
### Start all services first time
|
||||
|
||||
```bash
|
||||
# SSH into server
|
||||
ssh agm@agmission-1.agnav.com -p 22222
|
||||
|
||||
# Start all configured apps
|
||||
/home/agm/apps/pm2-apps/start_pm2_apps.sh
|
||||
```
|
||||
|
||||
### Reload after deployment
|
||||
|
||||
```bash
|
||||
# Reload individual app (zero-downtime)
|
||||
pm2 reload agmission-prod
|
||||
|
||||
# Reload all at once
|
||||
pm2 reload all
|
||||
|
||||
# Check status
|
||||
pm2 status
|
||||
pm2 logs agmission-prod --lines 30
|
||||
```
|
||||
|
||||
### Process list
|
||||
|
||||
```bash
|
||||
pm2 list
|
||||
```
|
||||
|
||||
Expected processes:
|
||||
|
||||
| Name | Status |
|
||||
|---|---|
|
||||
| `agmission-prod` | online |
|
||||
| `track_server` | online |
|
||||
| `gps_server-agnav` | online |
|
||||
| `gps_server-rap` | online |
|
||||
| `job_worker` | online |
|
||||
| `invoice_worker` | online |
|
||||
| `cleanup_worker` | online |
|
||||
| `partner_sync_worker` | online |
|
||||
| `partner_data_polling_worker` | online |
|
||||
|
||||
### Restart a single worker
|
||||
|
||||
```bash
|
||||
pm2 restart job_worker
|
||||
pm2 restart partner_sync_worker
|
||||
pm2 restart invoice_worker
|
||||
```
|
||||
|
||||
### Save current PM2 process list
|
||||
|
||||
```bash
|
||||
pm2 save
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8 Backup and Recovery
|
||||
|
||||
### Automated backup
|
||||
|
||||
`trunk/Others/scripts/backup_agm.sh` runs via cron. It:
|
||||
|
||||
1. Dumps MongoDB to a gzip archive (`mongodump --gzip`)
|
||||
2. Retains 11 days of database archives
|
||||
3. Syncs archives to NAS via `rsync` (`rsync://rsync@data.agnav.com/agm/`)
|
||||
4. Syncs uploaded job files separately
|
||||
5. Syncs rsync log files
|
||||
|
||||
### Manual MongoDB backup
|
||||
|
||||
```bash
|
||||
mongodump \
|
||||
--archive=/home/agm/backups/manual_$(date +%Y%m%d).gz \
|
||||
--gzip \
|
||||
--db agmission \
|
||||
--username agm \
|
||||
--authenticationDatabase agmission
|
||||
```
|
||||
|
||||
### Restore from backup
|
||||
|
||||
```bash
|
||||
mongorestore \
|
||||
--archive=/home/agm/backups/agmdb_YYYYMMDD.gz \
|
||||
--gzip \
|
||||
--db agmission \
|
||||
--username agm \
|
||||
--authenticationDatabase agmission \
|
||||
--drop
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9 Environment Variables Reference
|
||||
|
||||
Environment files live on the production server at `/home/agm/apps/agmission/environment.env` and are **not** managed by the deployment script.
|
||||
|
||||
### Core server
|
||||
|
||||
| Variable | Description |
|
||||
|---|---|
|
||||
| `AGM_PORT` | HTTP port the API server listens on (e.g., `7000`) |
|
||||
| `PRODUCTION` | `true` in production |
|
||||
| `DB_USR` / `DB_PWD` / `DB_NAME` | MongoDB credentials |
|
||||
| `DB_HOSTS` | MongoDB host list (replica set) |
|
||||
| `DB_REPLSET` | Replica set name (`rs0`) |
|
||||
| `REDIS_PWD` | Redis password |
|
||||
| `JWT_SECRET` | JWT signing secret |
|
||||
| `MAX_SESSION_SECS` | Session token lifetime (default: 28800) |
|
||||
|
||||
### Stripe billing
|
||||
|
||||
| Variable | Description |
|
||||
|---|---|
|
||||
| `STRIPE_SECRET_KEY` | Stripe secret key |
|
||||
| `STRIPE_PUBLISHABLE_KEY` | Stripe publishable key |
|
||||
| `STRIPE_API_VERSION` | Stripe API version |
|
||||
| `STRIPE_WH_SEC` | Stripe webhook signing secret |
|
||||
| `ESS_1` … `ESS_5` | Stripe price IDs for Essential plans |
|
||||
| `ENT_1` … `ENT_4` | Stripe price IDs for Enterprise plans |
|
||||
| `ADDON_1` | Stripe price ID for add-on |
|
||||
|
||||
### File storage
|
||||
|
||||
| Variable | Description |
|
||||
|---|---|
|
||||
| `UPLOAD_DIR` | Job upload directory |
|
||||
| `UNZIP_DIR` | Temp extraction directory |
|
||||
| `INV_UPLOAD_DIR` | Invoice image upload directory |
|
||||
| `INV_IMG_VIR_DIR` | Virtual URL path for invoice images |
|
||||
| `SATLOC_STORAGE_PATH` | Local storage for downloaded SatLoc log files |
|
||||
|
||||
### RabbitMQ
|
||||
|
||||
| Variable | Description |
|
||||
|---|---|
|
||||
| `QUEUE_HOST` | RabbitMQ host |
|
||||
| `QUEUE_PORT` | RabbitMQ port (default: 5672) |
|
||||
| `QUEUE_USR` / `QUEUE_PWD` | RabbitMQ credentials |
|
||||
| `QUEUE_VHOST` | Virtual host |
|
||||
| `QUEUE_NAME_JOBS` | Job processing queue name |
|
||||
| `QUEUE_NAME_GDATA` | GPS data queue name |
|
||||
| `QUEUE_NAME_PARTNER` | Partner tasks queue name |
|
||||
|
||||
### Partner integration
|
||||
|
||||
| Variable | Description |
|
||||
|---|---|
|
||||
| `SATLOC_API_ENDPOINT` | SatLoc Cloud base URL |
|
||||
| `SATLOC_API_TIMEOUT` | HTTP request timeout (ms) |
|
||||
| `PARTNER_SYNC_INTERVAL` | Partner sync interval (ms) |
|
||||
| `PARTNER_HEALTH_CHECK_INTERVAL` | Health check interval (ms) |
|
||||
| `PARTNER_MAX_RETRIES` | Max retry attempts before DLQ |
|
||||
| `DLQ_CHECK_INTERVAL` | DLQ monitoring interval (ms) |
|
||||
|
||||
---
|
||||
|
||||
*See [ARCHITECTURE.md](ARCHITECTURE.md) for full system design documentation.*
|
||||
@ -1,251 +0,0 @@
|
||||
# Data Export API — Proposal Summary
|
||||
|
||||
**Date:** April 7, 2026
|
||||
**Prepared by:** AgMission Team
|
||||
**For:** Customer Technical & Integration Team
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
Based on your requirements, we have analysed the current AgMission platform and designed an export API that will allow your team to pull mission data directly into your data infrastructure — data warehouse, Power BI, and ArcGIS — once per day or on demand.
|
||||
|
||||
The data available through this API is the same data currently displayed in the AgMission web application's **Data Playback** and **Report** screens.
|
||||
|
||||
---
|
||||
|
||||
## 2. What We Will Deliver
|
||||
|
||||
### 2.1 API Key Authentication
|
||||
|
||||
All API access will be secured using an **API Key** as requested. Keys will be managed from within the AgMission platform:
|
||||
|
||||
- Your applicator account can create and revoke keys independently (self-service)
|
||||
- AgMission platform administrators can also manage keys on your behalf
|
||||
- Each key is shown only once at creation and is stored securely
|
||||
- All requests must include the key in the request header (`X-API-Key`)
|
||||
|
||||
---
|
||||
|
||||
### 2.2 Job List Screen — Enhanced Filtering
|
||||
|
||||
The existing **Job List** screen in the AgMission web application will have its filter controls improved so users can quickly narrow the mission list before opening or exporting a specific job.
|
||||
|
||||
**Filter controls that will be available:**
|
||||
|
||||
| Filter | Description |
|
||||
|---|---|
|
||||
| Client | Dropdown selection |
|
||||
| Id N° | Search by mission ID |
|
||||
| Order Number | Full or partial match |
|
||||
| Mission Name | Partial match |
|
||||
| Start Date | From date (inclusive) |
|
||||
| End Date | To date (inclusive) |
|
||||
| Status | Mission status (e.g. Sprayed, Completed, etc.) |
|
||||
|
||||
---
|
||||
|
||||
### 2.3 Mission Session Summary
|
||||
|
||||
For each mission, one record per **application session** (uploaded data file). This covers the **Coverage with application information** requirement.
|
||||
|
||||
Each session record includes:
|
||||
|
||||
**Session identification & timing**
|
||||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| Session ID | Unique session identifier |
|
||||
| File Name | Original data file name |
|
||||
| Start / End Date & Time | UTC |
|
||||
| Total Flight Time | seconds |
|
||||
| Total Spray Time | seconds |
|
||||
| Total Turn Time | seconds |
|
||||
|
||||
**Application results**
|
||||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| Total Sprayed Area | Hectares actually covered |
|
||||
| Mapped Area | Planned area size from job definition (ha) |
|
||||
| Over-Spray % | `(Sprayed − Mapped) / Mapped × 100` |
|
||||
| Material Sprayed | Total volume or mass applied (litres or kg, metric) |
|
||||
| Material Type | Liquid (wet) or dry granular |
|
||||
| Target Application Rate | Planned rate from mission file |
|
||||
| Application Rate Unit | e.g. L/ha, Kg/ha |
|
||||
| Flow Controller | Equipment type used |
|
||||
| Average Spray Speed | Average ground speed during active spraying (m/s) |
|
||||
| Spray Zone Name | Named area or zone (Ag-NAV missions only) |
|
||||
| Spray Zone Area | Area size of the named zone (Ag-NAV only) |
|
||||
| Auto Spray On / Off Lag | Spray system timing offsets (seconds) |
|
||||
| Pulses per Litre | Flow calibration value (liquid Ag-NAV only) |
|
||||
|
||||
**Confirmed / adjusted values**
|
||||
|
||||
If the applicator has opened the **Report Settings** screen and confirmed the values, those confirmed values are returned. If they have **not** done so, the API automatically falls back to values calculated directly from the uploaded application data files — so this group is always populated.
|
||||
|
||||
A `reportConfirmed` flag indicates which case applies.
|
||||
|
||||
| Field | If report confirmed | If not confirmed (fallback) |
|
||||
|---|---|---|
|
||||
| Report Confirmed | `true` | `false` |
|
||||
| Area Size | Applicator-confirmed value (ha) | Calculated from planned spray area polygons |
|
||||
| Spray Coverage | Applicator-confirmed value (ha) | Sum of sprayed area across all sessions from data files |
|
||||
| Application Rate | Applicator-confirmed value | Target application rate from the data file |
|
||||
| Estimated Spray Volume | Coverage × Application Rate | Same formula using fallback values |
|
||||
| Actual Spray Volume | Manually entered by applicator (if provided) | Not available |
|
||||
| Effective Volume | Actual volume if entered, otherwise Estimated | Estimated Spray Volume (fallback) |
|
||||
|
||||
**Weather information** *(returned when manually entered by applicator in the absence of sensor data)*
|
||||
|
||||
| Field | Unit |
|
||||
|---|---|
|
||||
| Wind Speed | knots |
|
||||
| Wind Direction | Compass bearing (e.g. NE, SW) |
|
||||
| Temperature | °C |
|
||||
| Humidity | % |
|
||||
|
||||
**Pilot traceability**
|
||||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| Pilot Name (assigned) | Pilot assigned to the mission |
|
||||
| Pilot Name (from file) | Pilot name as recorded in the data file itself *(may differ if pilot changed in the field)* |
|
||||
| Pilot ID | Stable unique identifier |
|
||||
| Aircraft Tail Number | Registration number |
|
||||
| Aircraft Name | |
|
||||
| Assignment Date | Date the applicator officially assigned this pilot |
|
||||
|
||||
---
|
||||
|
||||
### 2.4 Raw GPS Trace Records — Applied Flow
|
||||
|
||||
The per-point GPS and application data that feeds the playback screen, returned as paginated records (up to 2,000 per request). This covers the **Applied Flow** requirement.
|
||||
|
||||
Each record corresponds to one GPS reading logged by the aircraft during the mission.
|
||||
|
||||
**GPS Data**
|
||||
|
||||
| Field | Unit | Description |
|
||||
|---|---|---|
|
||||
| Timestamp UTC | ISO 8601 | Local GPS time converted to UTC |
|
||||
| GPS Time | seconds | Raw GPS epoch seconds |
|
||||
| Latitude | decimal degrees | WGS84 |
|
||||
| Longitude | decimal degrees | WGS84 |
|
||||
| UTM X / Y | meters | UTM coordinates |
|
||||
| Altitude | meters | Above sea level |
|
||||
| Ground Speed | m/s | |
|
||||
| Heading | degrees | Direction of travel |
|
||||
| Cross-Track Error | meters | Deviation from planned line |
|
||||
| Locked Line | integer | Spray line number (Ag-NAV only) |
|
||||
| HDOP | — | GPS horizontal accuracy |
|
||||
| Satellites in View | count | |
|
||||
| Correction ID | — | GPS differential correction source |
|
||||
| WAAS ID | — | When applicable |
|
||||
| Spray Status | 0 / 1 | 0 = spray off, 1 = spray on |
|
||||
|
||||
> **Time interval filtering:** When requesting raw trace records or exporting, a `interval` parameter can be specified (e.g. 0.2 s, 0.4 s, 1 s, 5 s, 10 s) to return only one record per interval rather than every logged GPS point. This significantly reduces payload size for overview queries and large-batch exports.
|
||||
|
||||
**Application Info**
|
||||
|
||||
| Field | Unit | Description |
|
||||
|---|---|---|
|
||||
| Flow Rate Applied | L/min | Actual flow rate measured |
|
||||
| Flow Rate Required | L/min | Target flow rate |
|
||||
| Application Rate Required | L/ha or Kg/ha | Target rate per point |
|
||||
| Application Rate Applied | L/ha | Calculated from measured flow, speed, and swath |
|
||||
| Swath Width | meters | |
|
||||
| Boom Pressure | psi | Liquid Ag-NAV only |
|
||||
| Auto Spray On Lag | seconds | Spray-on activation delay (session constant, repeated per record) |
|
||||
| Auto Spray Off Lag | seconds | Spray-off deactivation delay (session constant, repeated per record) |
|
||||
| Pulses per Litre | count | Flow meter calibration value — liquid Ag-NAV only (session constant) |
|
||||
| RPM values | array | Up to 10 channels; semantics depend on material type (liquid vs. dry) |
|
||||
|
||||
**Meteorological (MET)**
|
||||
|
||||
| Field | Unit | Description |
|
||||
|---|---|---|
|
||||
| Wind Speed | m/s | Measured in-flight |
|
||||
| Wind Direction | degrees | |
|
||||
| Temperature | °C | |
|
||||
| Humidity | % | |
|
||||
|
||||
> **Note:** All values are in metric units. Date/times are UTC. Coordinates are in WGS84 (EPSG:4326), which is numerically equivalent to SIRGAS 2000 used in Brazil.
|
||||
|
||||
---
|
||||
|
||||
### 2.5 Bulk Export (CSV / GeoJSON Download)
|
||||
|
||||
For the daily 17:00 Brasília batch pull and ArcGIS imports, a **file-based export** endpoint is available:
|
||||
|
||||
1. **Request export** — call the export endpoint for a mission; the system generates the file asynchronously
|
||||
2. **Poll for status** — check whether the file is ready (typically seconds to a few minutes for large missions)
|
||||
3. **Download** — retrieve the file via the returned download link
|
||||
|
||||
**Available formats:** CSV (all raw trace data, one row per GPS record) and GeoJSON.
|
||||
|
||||
CSV files include job and session header columns repeated on every row for easy import directly into Power BI or a data warehouse without needing a separate join step.
|
||||
|
||||
---
|
||||
|
||||
### 2.6 Spray-Area Boundary Polygons *(Pending confirmation — see Section 4)*
|
||||
|
||||
An optional endpoint to retrieve the **planned field boundary polygons** as GeoJSON, for direct import as ArcGIS layers.
|
||||
|
||||
---
|
||||
|
||||
## 3. Data Units & Standards
|
||||
|
||||
| Aspect | Standard used |
|
||||
|---|---|
|
||||
| All measurements | Metric (ha, m/s, L/min, L/ha, Kg/ha, °C, meters) |
|
||||
| Dates & times | ISO 8601 UTC strings |
|
||||
| Coordinates | WGS84 decimal degrees (EPSG:4326 / numerically equivalent to SIRGAS 2000) |
|
||||
| Volumes | Litres for liquid, Kg for dry granular |
|
||||
| Speeds | m/s in raw records; km/h can be derived by consumer (× 3.6) |
|
||||
|
||||
---
|
||||
|
||||
## 4. Pending Confirmation — One Open Item
|
||||
|
||||
**Spray-area boundary polygons for ArcGIS**
|
||||
|
||||
> Do you need the **planned field boundary polygons** (the area outlines drawn in the mission) exported as GeoJSON geometry, in addition to the numeric coverage statistics (hectares)?
|
||||
>
|
||||
> - If **yes** — we will provide a dedicated endpoint returning the boundaries as GeoJSON features, suitable for direct layer import into ArcGIS.
|
||||
> - If **no** — the numeric coverage and session summary fields are sufficient and no additional endpoint is needed.
|
||||
>
|
||||
> *This is the only remaining item before implementation can begin.*
|
||||
|
||||
---
|
||||
|
||||
## 5. Estimated Delivery Timeline
|
||||
|
||||
| Team size | Estimated duration | Target delivery |
|
||||
|---|---|---|
|
||||
| 1 developer | ~5 weeks | ~mid-May 2026 |
|
||||
| 2 developers | ~3 weeks | ~end of April 2026 |
|
||||
|
||||
Includes development, testing, sandbox environment setup, and a review cycle.
|
||||
|
||||
A **sandbox environment with sample missions** will be made available for your team to validate the API responses before production go-live.
|
||||
|
||||
---
|
||||
|
||||
## 6. Summary of Requirements Confirmed
|
||||
|
||||
| Requirement | Status |
|
||||
|---|---|
|
||||
| API Key authentication | ✅ Confirmed — self-service key management for both the applicator account and AgMission admin |
|
||||
| Applied Flow data (per GPS point) | ✅ Confirmed — all fields from GPS Data and Applic Info tabs included |
|
||||
| Coverage with application info | ✅ Confirmed — session summaries include both raw aggregates and applicator-confirmed values |
|
||||
| Pilot Traceability | ✅ Confirmed — pilot name, ID, tail number, assignment date, and in-file pilot name all included |
|
||||
| Mission filtering (Client, Order No., Name, Dates) | ✅ Confirmed — UI enhancement to the existing Job List screen |
|
||||
| Pull delivery (not push) | ✅ Confirmed — pull endpoints + async file download |
|
||||
| Raw data (not calculated metrics) | ✅ Confirmed — all raw sensor values returned; calculated `appRateApplied` also included as it appears in the UI |
|
||||
| Power BI integration | ✅ Supported — cursor-paginated JSON for incremental refresh |
|
||||
| ArcGIS integration | ✅ Supported — GeoJSON export + boundary polygons (pending confirmation) |
|
||||
| Data warehouse / bulk load | ✅ Supported — CSV export endpoint |
|
||||
| Consumption once daily at 17:00 Brasília | ✅ Supported — async export endpoint designed for this pattern |
|
||||
| Sandbox environment | ✅ Will be provided before production go-live |
|
||||
| Spray-area boundary polygons | ⏳ **Pending your confirmation (Section 4)** |
|
||||
@ -1,391 +0,0 @@
|
||||
# Data Export API — Requirements & Solution Definition
|
||||
|
||||
**Date:** April 7, 2026
|
||||
**Status:** In Progress — Questions Q-A through Q-D resolved
|
||||
**Source documents:** Customer API requirements (Data Export API + API AGNAV sections)
|
||||
|
||||
---
|
||||
|
||||
## 1. Background
|
||||
|
||||
The customer (a grower/client) requires a data extraction API to pull mission data from the AgMission platform into their internal data infrastructure (data warehouse, Power BI, ArcGIS). The same data is already calculated and displayed in the web UI via the **Data Playback** function in `job-map-edit.component`.
|
||||
|
||||
Two functional areas are requested:
|
||||
|
||||
1. **Application data export** — expose the playback-computed data (GPS trace + application metrics) through a REST API.
|
||||
2. **Job List screen filtering (UI enhancement)** — improve the filter controls on the existing Job List screen so users can search/narrow missions by client, order number, name, date range, and status.
|
||||
|
||||
---
|
||||
|
||||
## 2. Customer Requirements Summary
|
||||
|
||||
| Category | Requirement |
|
||||
|---|---|
|
||||
| Data needed | Applied Flow, Coverage (with application info, not real-time only), Pilot Traceability |
|
||||
| Mission filter fields | UI enhancement on the existing Job List screen: Client / ID No. / Order No. / Name / Start Date / End Date |
|
||||
| Delivery method | Pull from API (not push) |
|
||||
| Authentication | API Key |
|
||||
| Compliance | None |
|
||||
| Data type | Raw data (no pre-calculated aggregates required beyond what is already stored) |
|
||||
| Consumption | Once per day at 17:00 Brasília time (UTC−3) |
|
||||
| Target platforms | Data warehouse, Power BI, ArcGIS |
|
||||
| Sandbox | Requested once API is ready |
|
||||
|
||||
---
|
||||
|
||||
## 3. Proposed API Features
|
||||
|
||||
### 3.1 API Key Authentication (Prerequisite)
|
||||
|
||||
No API key mechanism exists in the current codebase — the server currently uses JWT Bearer tokens only (see `middlewares/app_validator.js`).
|
||||
|
||||
**Design:**
|
||||
|
||||
- New `ApiKey` Mongoose model with fields: `owner` (ObjectId ref to applicator/`byPuid`), `name` (string label), `keyHash` (bcrypt-hashed), `active` (boolean), `createdAt`, `lastUsedAt`, `managedBy` (enum: `customer` | `admin`)
|
||||
- New Express middleware (parallel to `checkUser`) validating `X-API-Key` header against hashed keys
|
||||
- Key resolves to an applicator `byPuid`, so all existing ownership-scoping logic continues to work unchanged
|
||||
- Management: both the **master applicator account** (self-service via web UI) and the **AgMission platform admin** can create/revoke keys
|
||||
- All public API routes mounted under `/api/v1/` prefix with API-key middleware only
|
||||
|
||||
---
|
||||
|
||||
### 3.2 UI Enhancement 1 — Job List Screen Filtering
|
||||
|
||||
**Type:** Frontend (web UI) change only — not an API endpoint.
|
||||
|
||||
The existing Job List screen (`job-list` component) already shows columns for Client, Id N°, Order N°, Name, Start Date, End Date, and Status. The customer requirement is to **add or improve the interactive filter controls** on this screen so users can narrow the list easily.
|
||||
|
||||
**Required filter controls:**
|
||||
|
||||
| Filter | Behaviour |
|
||||
|---|---|
|
||||
| Client | Dropdown — filter by client name |
|
||||
| Id N° | Text search — partial or exact match |
|
||||
| Order N° | Text search — partial or exact match |
|
||||
| Name | Text search — partial match (case-insensitive) |
|
||||
| Start Date | Date picker — show jobs from this date |
|
||||
| End Date | Date picker — show jobs up to this date |
|
||||
| Status | Dropdown — All / Sprayed / Completed / etc. |
|
||||
|
||||
**Backend note:** The existing `searchJobs_post` / `getJobs_get` aggregation pipeline already supports most of these filters. This step primarily wires up the UI controls and ensures `orderNumber` and date-range parameters are accepted by the backend query.
|
||||
|
||||
---
|
||||
|
||||
### 3.3 Feature 2A — Session Summary per Job
|
||||
|
||||
**Endpoint:** `GET /api/v1/jobs/:jobId/sessions`
|
||||
|
||||
Returns one record per uploaded application file (one "session" = one `App` + its `AppFile` children). All values are already stored — no traversal of `AppDetail` needed.
|
||||
|
||||
**Response fields per session:**
|
||||
|
||||
| Field | Source model → field | Notes |
|
||||
|---|---|---|
|
||||
| `sessionId` | `App._id` | |
|
||||
| `fileName` | `App.fileName` | |
|
||||
| `startDateTime` | `App.startDateTime` | ISO 8601 UTC |
|
||||
| `endDateTime` | `App.endDateTime` | ISO 8601 UTC |
|
||||
| `totalFlightTime_s` | `App.totalFlightTime` | seconds |
|
||||
| `totalSprayTime_s` | `App.totalSprayTime` | seconds |
|
||||
| `totalTurnTime_s` | `App.totalTurnTime` | seconds |
|
||||
| `totalSprayed_ha` | `App.totalSprayed` | hectares |
|
||||
| `totalSprayMat` | `App.totalSprayMat` | L or Kg (metric) |
|
||||
| `totalSprayMatUnit` | `App.totalSprayMatUnit` | 3=L/ha, 4=Kg/ha |
|
||||
| `pilotName` | `AppFile.meta.operator` or `Job.operator.name` | Pilot traceability |
|
||||
| `sprayZoneName` | `AppFile.meta.areaOrZone` | AgNav only |
|
||||
| `sprayZoneArea_ha` | `AppFile.meta.sprCoverage[1]` | AgNav only |
|
||||
| `appRate` | `AppFile.meta.appRate` or `Job.appRate` | Target application rate |
|
||||
| `appRateUnit` | `AppFile.meta.appRateUnitStr` | String label |
|
||||
| `matType` | `AppFile.meta.matType` | `wet` or `dry` |
|
||||
| `flowController` | `AppFile.meta.fcName` | |
|
||||
| `sprayOnLag_s` | `AppFile.meta.sprOnLag` | seconds |
|
||||
| `sprayOffLag_s` | `AppFile.meta.sprOffLag` | seconds |
|
||||
| `pulsesPerLiter` | `AppFile.meta.pulsesPerLit` | Liquid AgNav only |
|
||||
| `overSprayedPct` | `(totalSprayed − mappedArea) / mappedArea × 100` | Computed from stored values |
|
||||
| `mappedArea_ha` | `Job.sprayAreas[].properties.area` (sum) | From job spray-area polygons |
|
||||
| `avgSpraySpeed_ms` | `App.avgSpraySpeed` (stored at import — see §9) | m/s — average ground speed during spray-on periods |
|
||||
|
||||
#### Confirmed Application Summary (from Report Settings, with fallback)
|
||||
|
||||
If the applicator has used the Report Settings dialog, the confirmed/overridden values are returned. If they have **not** (i.e. `rptOp` fields are null), the API falls back to values calculated from the uploaded data files so that this group is **always populated**. A `reportConfirmed` boolean signals which case applies.
|
||||
|
||||
| API field | Source model → field | Fallback (when `rptOp` not set) | Notes |
|
||||
|---|---|---|---|
|
||||
| `reportConfirmed` | `Job.rptOp.coverage != null` | `false` | Boolean flag |
|
||||
| `areaSize_ha` | `Job.rptOp.areaSize` | Sum of `job.sprayAreas[].properties.area` | ha |
|
||||
| `coverage_ha` | `Job.rptOp.coverage` | Sum of `App.totalSprayed` across sessions | ha |
|
||||
| `appRate` | `Job.rptOp.appRate` | `AppFile.meta.appRate` (first session, or null if absent) | L/ha or Kg/ha |
|
||||
| `sprayVolume` | `rptOp.coverage × rptOp.appRate` | `coverage_ha(fallback) × appRate(fallback)` | Estimated total volume |
|
||||
| `useActualVolume` | `Job.rptOp.useActualVol` | `false` | `true` only when applicator explicitly chose actual vol |
|
||||
| `actualVolume` | `Job.rptOp.actualVol` | `null` | Manually entered; only present when `useActualVolume = true` |
|
||||
| `effectiveVolume` | `useActualVol ? actualVol : sprayVolume` | `sprayVolume` (fallback) | The authoritative volume for this job |
|
||||
| `useCustomWeather` | `Job.useCustWI` | `false` | |
|
||||
| `weather.windSpeed_kt` | `Job.weatherInfo.windSpd` | omitted | Only present when `useCustomWeather = true` |
|
||||
| `weather.windDir` | `Job.weatherInfo.windDir` | omitted | Only present when `useCustomWeather = true` |
|
||||
| `weather.temp_c` | `Job.weatherInfo.temp` | omitted | Only present when `useCustomWeather = true` |
|
||||
| `weather.humidity_pct` | `Job.weatherInfo.humid` | omitted | Only present when `useCustomWeather = true` |
|
||||
|
||||
> **Design note (Q-A / Q-B / fallback):** `reportConfirmed = false` means the applicator has not yet reviewed the job in the Report Settings dialog. In this state the API returns auto-calculated values from `App` and `AppFile` data so that the consumer's data warehouse always has a usable record. When `reportConfirmed` later becomes `true` (applicator confirms), the consumer can re-fetch and update the stored row. The `isConfirmed` boundary is `rptOp.coverage != null`.
|
||||
>
|
||||
> **Spray-area boundary polygons (Q-A — pending customer clarification):** Whether `job.sprayAreas` GeoJSON polygons should be included in the session summary or exposed as a separate `/jobs/:id/areas` endpoint is pending confirmation from the customer regarding their ArcGIS polygon import workflow. Both options are straightforward to implement.
|
||||
|
||||
---
|
||||
|
||||
### 3.4 Feature 2B — Raw GPS Trace Records
|
||||
|
||||
**Endpoint:** `GET /api/v1/jobs/:jobId/sessions/:fileId/records`
|
||||
|
||||
Exposes the per-point `AppDetail` records that feed the playback UI. Cursor-paginated (same scheme as existing `filesdata_post`).
|
||||
|
||||
**Query parameters:** `after` (cursor), `limit` (default 500, max 2000), `interval` (float seconds, e.g. `0.2`, `0.4`, `1`, `5`, `10` — when specified, only the first record within each interval window is returned, reducing payload size for overview queries and large-batch exports)
|
||||
|
||||
**Response fields per record (all raw values, metric units):**
|
||||
|
||||
#### GPS Data group
|
||||
|
||||
| API field | Source field | Unit | Notes |
|
||||
|---|---|---|---|
|
||||
| `timeUtc` | derived from `gpsTime` | ISO 8601 UTC string | |
|
||||
| `lat` | `AppDetail.lat` | decimal degrees WGS84 | |
|
||||
| `lon` | `AppDetail.lon` | decimal degrees WGS84 | |
|
||||
| `utmX` | `AppDetail.utmX` | meters | |
|
||||
| `utmY` | `AppDetail.utmY` | meters | |
|
||||
| `alt` | `AppDetail.alt` | meters ASL | |
|
||||
| `grSpeed` | `AppDetail.grSpeed` | m/s | |
|
||||
| `heading` | `AppDetail.head` | degrees | |
|
||||
| `xTrack` | `AppDetail.xTrack` | meters | Cross-track error |
|
||||
| `lockedLine` | `AppDetail.llnum` | integer | AgNav only |
|
||||
| `hdop` | `AppDetail.stdHdop` | float | |
|
||||
| `satsInView` | `AppDetail.satsIn` decoded | integer | `satsIn > 99 ? satsIn−100 : satsIn` |
|
||||
| `correctionId` | `AppDetail.tslu` decoded | integer | `tslu > 100 ? tslu−100 : tslu` |
|
||||
| `waasId` | `AppDetail.calcodeFreq` decoded | integer | Only if `calcodeFreq` in 20001–29999 |
|
||||
| `sprayStat` | `AppDetail.sprayStat` | 0 or 1 | 0=off, 1=on |
|
||||
|
||||
#### Applic Info group
|
||||
|
||||
| API field | Source field | Unit | Notes |
|
||||
|---|---|---|---|
|
||||
| `flowRateApplied` | `AppDetail.lminApp` | L/min | |
|
||||
| `flowRateRequired` | `AppDetail.lminReq` | L/min | |
|
||||
| `appRateRequired` | `AppDetail.lhaReq` | L/ha or Kg/ha | SatLoc per-point value |
|
||||
| `appRateApplied` | derived: `lminApp / (grSpeed × swath) × 10000` | L/ha | **Only computed field in raw trace** — see Note 1 |
|
||||
| `swathWidth` | `AppDetail.swath` | meters | |
|
||||
| `boomPressure_psi` | `AppDetail.psi` | psi | AgNav liquid only |
|
||||
| `sprayOnLag_s` | `AppFile.meta.sprOnLag` | seconds | Session constant — repeated per record |
|
||||
| `sprayOffLag_s` | `AppFile.meta.sprOffLag` | seconds | Session constant — repeated per record |
|
||||
| `pulsesPerLiter` | `AppFile.meta.pulsesPerLit` | count | Liquid AgNav only; session constant — repeated per record |
|
||||
| `rpm` | `AppDetail.rpm[0..9]` | array | See Note 2 for dry vs. liquid semantics |
|
||||
|
||||
#### MET group
|
||||
|
||||
| API field | Source field | Unit | Notes |
|
||||
|---|---|---|---|
|
||||
| `windSpeed_ms` | `AppDetail.windSpd` | m/s | |
|
||||
| `windDir_deg` | `AppDetail.windDir` | degrees | |
|
||||
| `temp_c` | `AppDetail.temp` | °C | |
|
||||
| `humidity_pct` | `AppDetail.humid` | % | |
|
||||
|
||||
> **Note 1 — appRateApplied:** This is the only derived value computed from raw fields. Formula: `appRateApplied = lminApp / (grSpeed_m_per_s × swath_m) × 10000`. If `grSpeed = 0` or `swath = 0`, return `null` to avoid division by zero. The UI equivalent is `PlayRecord.appRateAp`.
|
||||
>
|
||||
> **Note 2 — rpm array semantics:**
|
||||
> - Liquid material: indices 0–9 = RPM pairs 1/2 through 9/10 (pump RPM channels)
|
||||
> - Dry material: index 0–1 = AppRPM 1/2; index 2–3 = TarRPM 1/2; index 4 = GFC VIn; index 6–7 = Revs/Kg (× 0.453592 for Revs/Lb); index 8–9 = Amp 1/2
|
||||
> - `matType` (from session summary) determines which interpretation applies
|
||||
|
||||
---
|
||||
|
||||
### 3.5 Feature 2C — Export File (Async Download)
|
||||
|
||||
**Endpoints:**
|
||||
- `POST /api/v1/jobs/:jobId/export` — trigger export generation, returns `{ exportId, status: "pending" }`
|
||||
- `GET /api/v1/exports/:exportId` — poll status; when `status: "ready"`, includes `downloadUrl`
|
||||
|
||||
Reuses the existing temp-file infrastructure from `preAppReport_post` (`env.TEMP_DIR`, `env.REPORT_DIR`). Useful for ArcGIS bulk imports and the one-a-day batch pull at 17:00 Brasília.
|
||||
|
||||
**Supported formats:** `csv`, `geojson` (query param `?format=csv`)
|
||||
|
||||
**CSV columns:** All raw trace fields above, one row per `AppDetail` record, session/job header fields repeated for join convenience (`jobId`, `orderNumber`, `fileId`, `fileName`, `pilotName`).
|
||||
|
||||
---
|
||||
|
||||
## 4. Data Architecture — Source Mapping Summary
|
||||
|
||||
```
|
||||
Job → 3.2 Job List UI filters, 3.3 Session Summary (mappedArea)
|
||||
├── App → 3.3 Session Summary (times, volumes, totalSprayed)
|
||||
│ └── AppFile → 3.3 Session Summary (meta: operator, areaOrZone, fcName, etc.)
|
||||
│ └── AppDetail → 3.4 Raw GPS Trace records (all per-point fields)
|
||||
└── sprayAreas[] → 3.3 mappedArea_ha (sum of properties.area)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Volume & Pagination Strategy
|
||||
|
||||
**AppDetail** is indexed at billion+ document scale (see `model/application_detail.js` — `fileId` index, `_id` for cursor).
|
||||
|
||||
| Endpoint | Pagination | Typical volume |
|
||||
|---|---|---|
|
||||
| Session summary | None (small) | 1–20 per job |
|
||||
| Raw trace records | Cursor on `_id`, default 500/page | 10K–500K+ per file |
|
||||
| Export file | None (async, full download) | Unlimited |
|
||||
|
||||
> **Note:** Job listing/filtering is a UI screen enhancement (§3.2), not a standalone API endpoint. The export and session endpoints accept `jobId` directly.
|
||||
|
||||
The daily batch at 17:00 Brasília is best served by the **Export File (3.5)** approach. The cursor-paginated records endpoint (3.4) is for Power BI incremental refresh or selective queries.
|
||||
|
||||
---
|
||||
|
||||
## 6. Authentication & Key Management
|
||||
|
||||
| Actor | Can create keys | Can revoke keys | Scope |
|
||||
|---|---|---|---|
|
||||
| AgMission platform admin | Yes (any applicator) | Yes (any) | Any applicator's data |
|
||||
| Master applicator account | Yes (own account) | Yes (own) | Own clients/jobs only |
|
||||
|
||||
- API key is passed in `X-API-Key` request header
|
||||
- Keys are stored hashed (bcrypt); plain key shown only once at creation
|
||||
- Key resolves to `byPuid` (applicator), all existing ownership filters continue to apply
|
||||
- Rate limiting applies (reuse existing `express-rate-limit` config in `server.js`)
|
||||
|
||||
---
|
||||
|
||||
## 7. Questions & Resolutions
|
||||
|
||||
### Q-A — Coverage fields & confirmed aggregates ✅ Resolved
|
||||
|
||||
**Answer:** The API must expose both the system-calculated aggregates AND the user-confirmed/adjusted values from the Report Settings dialog. See the confirmed application summary table in §3.3.
|
||||
|
||||
The Report Settings dialog (screenshot) shows the following adjustable fields stored in `Job.rptOp` and `Job.weatherInfo`:
|
||||
- **Area Size** (`rptOp.areaSize`) — user-confirmed plan area with green checkmark
|
||||
- **Spray Coverage** (`rptOp.coverage`) — confirmed sprayed area
|
||||
- **AppRate** (`rptOp.appRate`) — confirmed application rate
|
||||
- **Spray Volume** — calculated (`coverage × appRate`), not stored separately
|
||||
- **Actual Spray Volume** (`rptOp.actualVol` + `rptOp.useActualVol` toggle) — optional manual override
|
||||
- **Weather Info** (`Job.useCustWI` + `Job.weatherInfo`) — manual weather if sensor data unavailable
|
||||
|
||||
**Spray-area boundary polygons:** Whether to include `job.sprayAreas` GeoJSON for ArcGIS import is **pending customer confirmation**. Recommended: expose as a separate optional endpoint `GET /api/v1/jobs/:id/areas` to avoid bloating the session summary response.
|
||||
|
||||
---
|
||||
|
||||
### Q-B — Calculated vs. raw values ✅ Resolved
|
||||
|
||||
**Answer:** The API returns both. Specifically:
|
||||
- **`appRateApplied`** — computed per-point in the raw trace (`lminApp / (grSpeed × swath) × 10000`). This is the only in-flight calculation in the records endpoint, matching what the playback UI displays.
|
||||
- **`avgSpraySpeed_ms`** — stored at import time in the `App` model (see Q-D). Returned from the session summary endpoint with no on-the-fly cost.
|
||||
- **Confirmed aggregates** — from `Job.rptOp` as described in Q-A. The consumer receives system-calculated values AND the applicator's manually confirmed values and can decide which to use for their reports.
|
||||
|
||||
---
|
||||
|
||||
### Q-C — Pilot Traceability scope ✅ Resolved with recommendations
|
||||
|
||||
**Answer and recommendations:**
|
||||
|
||||
File-level `pilotName` per session is the primary traceability field and matches what is recorded in the data file itself (`AppFile.meta.operator`). The following additional fields are recommended to make traceability robust:
|
||||
|
||||
| Additional field | Source | Rationale |
|
||||
|---|---|---|
|
||||
| `pilotId` | `Job.operator` (ObjectId) | Stable identifier — name strings can change or have duplicates across missions |
|
||||
| `aircraftName` | `Job.vehicle.name` | Aircraft identifier alongside pilot for fleet operations |
|
||||
| `aircraftTailNumber` | `Vehicle.tailNumber` | ANAC / FAA registration number; standard traceability field in Brazil |
|
||||
| `assignedDate` | `JobAssign.createdAt` | When the applicator officially assigned this pilot to the job |
|
||||
| `sessionPilotName` | `AppFile.meta.operator` | Pilot name as recorded in the data file itself (may differ from assigned pilot if swapped in the field) |
|
||||
|
||||
> **Multi-pilot and fleet note:** When multiple aircraft work the same job, each `AppFile` has its own `meta.operator`. The session summary (§3.3) returns one record per file, so traceability is inherently per-session. No per-GPS-point pilot attribution is needed — it would inflate the raw trace response with a constant repeated string.
|
||||
|
||||
> **Recommendation:** Include `pilotId` + `aircraftTailNumber` in both the job listing (§3.2) and the session summary (§3.3). Do not repeat in per-point records.
|
||||
|
||||
---
|
||||
|
||||
### Q-D — `AvgSprSpd` storage strategy ✅ Resolved
|
||||
|
||||
**Answer:** Store `avgSpraySpeed` at import time, in the `App` model alongside the existing aggregate fields (`totalFlightTime`, `totalSprayTime`, `totalSprayed`, etc.).
|
||||
|
||||
**Why not compute on-the-fly:** The `GET /api/v1/jobs/:jobId/sessions` endpoint (session summary) is designed to return only values already stored in `App` and `AppFile` — no `AppDetail` traversal. If `avgSpraySpeed` were computed on the fly at query time, it would require scanning potentially hundreds of thousands of `AppDetail` records per session, defeating the purpose of pre-aggregated session data.
|
||||
|
||||
**Implementation:** During file import processing (in the existing import worker/service), add the same accumulation logic that the playback UI uses:
|
||||
```
|
||||
if (sprayStat === 1) { totalSpraySpeed += grSpeed; sprayPointCount++; }
|
||||
avgSpraySpeed = sprayPointCount > 0 ? totalSpraySpeed / sprayPointCount : 0;
|
||||
```
|
||||
Store the result in a new `App.avgSpraySpeed` field (m/s, metric). No existing import consumers are affected.
|
||||
|
||||
---
|
||||
|
||||
## 8. Implementation Plan
|
||||
|
||||
### 8.1 Step-by-step breakdown with estimates
|
||||
|
||||
Estimates are in **working days** per developer, based on codebase familiarity with the existing patterns (existing cursor pagination, `job_worker.js` aggregate pattern, `preAppReport_post` temp-file infra, Angular service + component structure).
|
||||
|
||||
| Step | Feature | Days (1 dev) | Notes |
|
||||
|---|---|---|---|
|
||||
| 1 | `App.avgSpraySpeed` — add field to model + compute in `job_worker.js` + back-fill migration script | 2 d | `job_worker.js` lines ~519–526 already show the exact insertion point alongside `totalSprayed`, `totalSprayTime`, etc. Migration script iterates `AppDetail` cursor per `fileId`. |
|
||||
| 2 | `ApiKey` model + `checkApiKey` middleware + CRUD routes (create/list/revoke) | 3 d | New Mongoose model, bcrypt hash, new Express middleware parallel to `checkUser`. Admin and customer scopes via role check. |
|
||||
| 3 | Job List screen filter enhancements (UI) | 1.5 d | Wire up `orderNumber`, date-range, and client dropdown filter controls in the `job-list` component. Ensure existing `searchJobs_post` pipeline accepts these params; minor backend query update if missing. |
|
||||
| 4 | `GET /api/v1/jobs/:id/sessions` — session summary | 2.5 d | Joins `App` + `AppFile` + `Job.rptOp` + `Job.weatherInfo`. Adds `avgSpraySpeed`, confirmed-aggregate fields, pilot traceability fields. |
|
||||
| 5 | `GET /api/v1/jobs/:id/sessions/:fileId/records` — raw trace | 2.5 d | Wraps existing `filesdata_post` cursor logic. Adds field mapping/decoding (`satsIn`, `tslu`, `calcodeFreq`, `sprayStat=3` filter, `appRateApplied` formula). |
|
||||
| 6 | `GET /api/v1/jobs/:id/areas` — spray-area GeoJSON | 1 d | Single aggregation on `job.sprayAreas`. Trivial once route infra is in place. |
|
||||
| 7 | `POST /api/v1/jobs/:id/export` + `GET /api/v1/exports/:id` — async CSV/GeoJSON | 4 d | Node.js stream-based CSV writer over `AppDetail` cursor. Status polling. Reuses `env.TEMP_DIR` / `env.REPORT_DIR` temp-file pattern from `preAppReport_post`. |
|
||||
| 8 | Key management UI (Angular) | 3.5 d | New settings page: list keys, generate (show once), revoke. Standard Angular service + PrimeNG table, same pattern as existing settings components. |
|
||||
| 9 | Sandbox data seeding script | 1 d | Script to insert representative sample jobs, applications, and AppDetail records for a test applicator account. |
|
||||
| — | **Testing, code review, bug fixes (~20% buffer)** | 4 d | Unit tests for middleware and field calculations; integration tests against sandbox. |
|
||||
| | **Total** | **25 d** | |
|
||||
|
||||
---
|
||||
|
||||
### 8.2 Timeline by team size
|
||||
|
||||
#### 1 Developer — ~5 weeks
|
||||
|
||||
```
|
||||
Week 1 Steps 1–3 avgSpraySpeed import field, API key infra, job listing
|
||||
Week 2 Steps 4–5 Session summary + raw trace records endpoints
|
||||
Week 3 Steps 6–7 Areas GeoJSON endpoint + async export
|
||||
Week 4 Step 8 Key management UI
|
||||
Week 5 Step 9 + buffer Sandbox seeding + testing/review/fixes
|
||||
```
|
||||
|
||||
**Delivery: end of Week 5**
|
||||
|
||||
---
|
||||
|
||||
#### 2 Developers — ~3 weeks
|
||||
|
||||
Split backend (Dev A) and frontend + export (Dev B) in parallel once API key middleware (Step 2) is done on Day 3:
|
||||
|
||||
```
|
||||
Dev A (Backend) Dev B (Frontend + Export)
|
||||
Week 1 Step 1 (2d) → Step 2 (3d) Step 2 unblocks Day 3:
|
||||
Step 8 Key management UI (3.5d, starts Day 3)
|
||||
Week 2 Step 3 (1.5d) → Step 4 (2.5d) Finish Step 8 → Step 7 async export (4d)
|
||||
Week 3 Step 5 (2.5d) → Step 6 (1d) Finish Step 7 → Step 9 sandbox (1d)
|
||||
→ buffer/testing (1.5d) → integration testing (1.5d)
|
||||
```
|
||||
|
||||
**Delivery: end of Week 3**
|
||||
|
||||
---
|
||||
|
||||
### 8.3 Risk & assumptions
|
||||
|
||||
| Risk | Likelihood | Mitigation |
|
||||
|---|---|---|
|
||||
| Back-fill migration for `avgSpraySpeed` is slow on large `AppDetail` collections | Medium | Run as offline batch with cursor + bulk write; add progress logging |
|
||||
| Consumer's Power BI connector requires specific pagination or auth header format | Low | Validate against sandbox before sign-off; adjust header/response format if needed |
|
||||
| Async export generation times out for very large jobs (500K+ records) | Medium | Stream CSV via Node.js `Transform` instead of loading all records into memory; set job-level export size warning |
|
||||
| Spray-area polygon GeoJSON payload size (Step 6) | Low | Polygons are already stored simplified in `job.sprayAreas`; response stays small |
|
||||
|
||||
---
|
||||
|
||||
## 9. Notes & Constraints
|
||||
|
||||
- All API responses use **metric units** internally (ha, m/s, L/min, °C, meters). Unit conversion is the consumer's responsibility.
|
||||
- All dates/times returned as **ISO 8601 UTC strings**.
|
||||
- Coordinates in **WGS84 decimal degrees** (EPSG:4326). If SIRGAS 2000 (EPSG:4674) is needed for ArcGIS Brazil, note that it is numerically identical to WGS84 for practical purposes.
|
||||
- The `raserAlt` field in `AppDetail` schema has a typo (should be `laserAlt`). The API exposes it as `laserAlt_m` regardless.
|
||||
- `AppDetail.sprayStat` value `0` = spray off, `1` = spray on. Value `3` is an end-of-segment marker used internally; the API should filter it out or map it to `0`.
|
||||
- Existing `filesdata_post` cursor pagination uses the `_id` field index — the same scheme is reused for the public records endpoint.
|
||||
- `App.avgSpraySpeed` is a **new field** to be added to the `App` Mongoose model and populated during import processing. It must be back-filled for existing jobs (one-time migration script over existing `AppDetail` records).
|
||||
- The session summary endpoint (`GET /api/v1/jobs/:jobId/sessions`) is intentionally a **lightweight endpoint** — it reads only from `App` and `AppFile` models, never from `AppDetail`. This is why `avgSpraySpeed` must be pre-computed and stored rather than derived at query time.
|
||||
- **Remaining open item:** `job.sprayAreas` GeoJSON polygon inclusion — pending customer confirmation on ArcGIS integration requirements (see Q-A).
|
||||
@ -1,251 +0,0 @@
|
||||
# Data Export API — Proposal Summary
|
||||
|
||||
**Date:** April 7, 2026
|
||||
**Prepared by:** AgMission Team
|
||||
**For:** Customer Technical & Integration Team
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
Based on your requirements, we have analysed the current AgMission platform and designed an export API that will allow your team to pull mission data directly into your data infrastructure — data warehouse, Power BI, and ArcGIS — once per day or on demand.
|
||||
|
||||
The data available through this API is the same data currently displayed in the AgMission web application's **Data Playback** and **Report** screens.
|
||||
|
||||
---
|
||||
|
||||
## 2. What We Will Deliver
|
||||
|
||||
### 2.1 API Key Authentication
|
||||
|
||||
All API access will be secured using an **API Key** as requested. Keys will be managed from within the AgMission platform:
|
||||
|
||||
- Your applicator account can create and revoke keys independently (self-service)
|
||||
- AgMission platform administrators can also manage keys on your behalf
|
||||
- Each key is shown only once at creation and is stored securely
|
||||
- All requests must include the key in the request header (`X-API-Key`)
|
||||
|
||||
---
|
||||
|
||||
### 2.2 Job List Screen — Enhanced Filtering
|
||||
|
||||
The existing **Job List** screen in the AgMission web application will have its filter controls improved so users can quickly narrow the mission list before opening or exporting a specific job.
|
||||
|
||||
**Filter controls that will be available:**
|
||||
|
||||
| Filter | Description |
|
||||
|---|---|
|
||||
| Client | Dropdown selection |
|
||||
| Id N° | Search by mission ID |
|
||||
| Order Number | Full or partial match |
|
||||
| Mission Name | Partial match |
|
||||
| Start Date | From date (inclusive) |
|
||||
| End Date | To date (inclusive) |
|
||||
| Status | Mission status (e.g. Sprayed, Completed, etc.) |
|
||||
|
||||
---
|
||||
|
||||
### 2.3 Mission Session Summary
|
||||
|
||||
For each mission, one record per **application session** (uploaded data file). This covers the **Coverage with application information** requirement.
|
||||
|
||||
Each session record includes:
|
||||
|
||||
**Session identification & timing**
|
||||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| Session ID | Unique session identifier |
|
||||
| File Name | Original data file name |
|
||||
| Start / End Date & Time | UTC |
|
||||
| Total Flight Time | seconds |
|
||||
| Total Spray Time | seconds |
|
||||
| Total Turn Time | seconds |
|
||||
|
||||
**Application results**
|
||||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| Total Sprayed Area | Hectares actually covered |
|
||||
| Mapped Area | Planned area size from job definition (ha) |
|
||||
| Over-Spray % | `(Sprayed − Mapped) / Mapped × 100` |
|
||||
| Material Sprayed | Total volume or mass applied (litres or kg, metric) |
|
||||
| Material Type | Liquid (wet) or dry granular |
|
||||
| Target Application Rate | Planned rate from mission file |
|
||||
| Application Rate Unit | e.g. L/ha, Kg/ha |
|
||||
| Flow Controller | Equipment type used |
|
||||
| Average Spray Speed | Average ground speed during active spraying (m/s) |
|
||||
| Spray Zone Name | Named area or zone (Ag-NAV missions only) |
|
||||
| Spray Zone Area | Area size of the named zone (Ag-NAV only) |
|
||||
| Auto Spray On / Off Lag | Spray system timing offsets (seconds) |
|
||||
| Pulses per Litre | Flow calibration value (liquid Ag-NAV only) |
|
||||
|
||||
**Confirmed / adjusted values**
|
||||
|
||||
If the applicator has opened the **Report Settings** screen and confirmed the values, those confirmed values are returned. If they have **not** done so, the API automatically falls back to values calculated directly from the uploaded application data files — so this group is always populated.
|
||||
|
||||
A `reportConfirmed` flag indicates which case applies.
|
||||
|
||||
| Field | If report confirmed | If not confirmed (fallback) |
|
||||
|---|---|---|
|
||||
| Report Confirmed | `true` | `false` |
|
||||
| Area Size | Applicator-confirmed value (ha) | Calculated from planned spray area polygons |
|
||||
| Spray Coverage | Applicator-confirmed value (ha) | Sum of sprayed area across all sessions from data files |
|
||||
| Application Rate | Applicator-confirmed value | Target application rate from the data file |
|
||||
| Estimated Spray Volume | Coverage × Application Rate | Same formula using fallback values |
|
||||
| Actual Spray Volume | Manually entered by applicator (if provided) | Not available |
|
||||
| Effective Volume | Actual volume if entered, otherwise Estimated | Estimated Spray Volume (fallback) |
|
||||
|
||||
**Weather information** *(returned when manually entered by applicator in the absence of sensor data)*
|
||||
|
||||
| Field | Unit |
|
||||
|---|---|
|
||||
| Wind Speed | knots |
|
||||
| Wind Direction | Compass bearing (e.g. NE, SW) |
|
||||
| Temperature | °C |
|
||||
| Humidity | % |
|
||||
|
||||
**Pilot traceability**
|
||||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| Pilot Name (assigned) | Pilot assigned to the mission |
|
||||
| Pilot Name (from file) | Pilot name as recorded in the data file itself *(may differ if pilot changed in the field)* |
|
||||
| Pilot ID | Stable unique identifier |
|
||||
| Aircraft Tail Number | Registration number |
|
||||
| Aircraft Name | |
|
||||
| Assignment Date | Date the applicator officially assigned this pilot |
|
||||
|
||||
---
|
||||
|
||||
### 2.4 Raw GPS Trace Records — Applied Flow
|
||||
|
||||
The per-point GPS and application data that feeds the playback screen, returned as paginated records (up to 2,000 per request). This covers the **Applied Flow** requirement.
|
||||
|
||||
Each record corresponds to one GPS reading logged by the aircraft during the mission.
|
||||
|
||||
**GPS Data**
|
||||
|
||||
| Field | Unit | Description |
|
||||
|---|---|---|
|
||||
| Timestamp UTC | ISO 8601 | Local GPS time converted to UTC |
|
||||
| GPS Time | seconds | Raw GPS epoch seconds |
|
||||
| Latitude | decimal degrees | WGS84 |
|
||||
| Longitude | decimal degrees | WGS84 |
|
||||
| UTM X / Y | meters | UTM coordinates |
|
||||
| Altitude | meters | Above sea level |
|
||||
| Ground Speed | m/s | |
|
||||
| Heading | degrees | Direction of travel |
|
||||
| Cross-Track Error | meters | Deviation from planned line |
|
||||
| Locked Line | integer | Spray line number (Ag-NAV only) |
|
||||
| HDOP | — | GPS horizontal accuracy |
|
||||
| Satellites in View | count | |
|
||||
| Correction ID | — | GPS differential correction source |
|
||||
| WAAS ID | — | When applicable |
|
||||
| Spray Status | 0 / 1 | 0 = spray off, 1 = spray on |
|
||||
|
||||
> **Time interval filtering:** When requesting raw trace records or exporting, a `interval` parameter can be specified (e.g. 0.2 s, 0.4 s, 1 s, 5 s, 10 s) to return only one record per interval rather than every logged GPS point. This significantly reduces payload size for overview queries and large-batch exports.
|
||||
|
||||
**Application Info**
|
||||
|
||||
| Field | Unit | Description |
|
||||
|---|---|---|
|
||||
| Flow Rate Applied | L/min | Actual flow rate measured |
|
||||
| Flow Rate Required | L/min | Target flow rate |
|
||||
| Application Rate Required | L/ha or Kg/ha | Target rate per point |
|
||||
| Application Rate Applied | L/ha | Calculated from measured flow, speed, and swath |
|
||||
| Swath Width | meters | |
|
||||
| Boom Pressure | psi | Liquid Ag-NAV only |
|
||||
| Auto Spray On Lag | seconds | Spray-on activation delay (session constant, repeated per record) |
|
||||
| Auto Spray Off Lag | seconds | Spray-off deactivation delay (session constant, repeated per record) |
|
||||
| Pulses per Litre | count | Flow meter calibration value — liquid Ag-NAV only (session constant) |
|
||||
| RPM values | array | Up to 10 channels; semantics depend on material type (liquid vs. dry) |
|
||||
|
||||
**Meteorological (MET)**
|
||||
|
||||
| Field | Unit | Description |
|
||||
|---|---|---|
|
||||
| Wind Speed | m/s | Measured in-flight |
|
||||
| Wind Direction | degrees | |
|
||||
| Temperature | °C | |
|
||||
| Humidity | % | |
|
||||
|
||||
> **Note:** All values are in metric units. Date/times are UTC. Coordinates are in WGS84 (EPSG:4326), which is numerically equivalent to SIRGAS 2000 used in Brazil.
|
||||
|
||||
---
|
||||
|
||||
### 2.5 Bulk Export (CSV / GeoJSON Download)
|
||||
|
||||
For the daily 17:00 Brasília batch pull and ArcGIS imports, a **file-based export** endpoint is available:
|
||||
|
||||
1. **Request export** — call the export endpoint for a mission; the system generates the file asynchronously
|
||||
2. **Poll for status** — check whether the file is ready (typically seconds to a few minutes for large missions)
|
||||
3. **Download** — retrieve the file via the returned download link
|
||||
|
||||
**Available formats:** CSV (all raw trace data, one row per GPS record) and GeoJSON.
|
||||
|
||||
CSV files include job and session header columns repeated on every row for easy import directly into Power BI or a data warehouse without needing a separate join step.
|
||||
|
||||
---
|
||||
|
||||
### 2.6 Spray-Area Boundary Polygons *(Pending confirmation — see Section 4)*
|
||||
|
||||
An optional endpoint to retrieve the **planned field boundary polygons** as GeoJSON, for direct import as ArcGIS layers.
|
||||
|
||||
---
|
||||
|
||||
## 3. Data Units & Standards
|
||||
|
||||
| Aspect | Standard used |
|
||||
|---|---|
|
||||
| All measurements | Metric (ha, m/s, L/min, L/ha, Kg/ha, °C, meters) |
|
||||
| Dates & times | ISO 8601 UTC strings |
|
||||
| Coordinates | WGS84 decimal degrees (EPSG:4326 / numerically equivalent to SIRGAS 2000) |
|
||||
| Volumes | Litres for liquid, Kg for dry granular |
|
||||
| Speeds | m/s in raw records; km/h can be derived by consumer (× 3.6) |
|
||||
|
||||
---
|
||||
|
||||
## 4. Pending Confirmation — One Open Item
|
||||
|
||||
**Spray-area boundary polygons for ArcGIS**
|
||||
|
||||
> Do you need the **planned field boundary polygons** (the area outlines drawn in the mission) exported as GeoJSON geometry, in addition to the numeric coverage statistics (hectares)?
|
||||
>
|
||||
> - If **yes** — we will provide a dedicated endpoint returning the boundaries as GeoJSON features, suitable for direct layer import into ArcGIS.
|
||||
> - If **no** — the numeric coverage and session summary fields are sufficient and no additional endpoint is needed.
|
||||
>
|
||||
> *This is the only remaining item before implementation can begin.*
|
||||
|
||||
---
|
||||
|
||||
## 5. Estimated Delivery Timeline
|
||||
|
||||
| Phase | Estimated duration | Target delivery |
|
||||
|---|---|---|
|
||||
| 1 | ~4 weeks | ~mid-May 2026 |
|
||||
| 2 | ~2 weeks | ~end of May 2026 |
|
||||
|
||||
Includes development, testing, sandbox environment setup, and a review cycle.
|
||||
|
||||
A **sandbox environment with sample missions** will be made available for your team to validate the API responses before production go-live.
|
||||
|
||||
---
|
||||
|
||||
## 6. Summary of Requirements Confirmed
|
||||
|
||||
| Requirement | Status |
|
||||
|---|---|
|
||||
| API Key authentication | ✅ Confirmed — self-service key management for both the applicator account and AgMission admin |
|
||||
| Applied Flow data (per GPS point) | ✅ Confirmed — all fields from GPS Data and Applic Info tabs included |
|
||||
| Coverage with application info | ✅ Confirmed — session summaries include both raw aggregates and applicator-confirmed values |
|
||||
| Pilot Traceability | ✅ Confirmed — pilot name, ID, tail number, assignment date, and in-file pilot name all included |
|
||||
| Mission filtering (Client, Order No., Name, Dates) | ✅ Confirmed — UI enhancement to the existing Job List screen |
|
||||
| Pull delivery (not push) | ✅ Confirmed — pull endpoints + async file download |
|
||||
| Raw data (not calculated metrics) | ✅ Confirmed — all raw sensor values returned; calculated `appRateApplied` also included as it appears in the UI |
|
||||
| Power BI integration | ✅ Supported — cursor-paginated JSON for incremental refresh |
|
||||
| ArcGIS integration | ✅ Supported — GeoJSON export + boundary polygons (pending confirmation) |
|
||||
| Data warehouse / bulk load | ✅ Supported — CSV export endpoint |
|
||||
| Consumption once daily at 17:00 Brasília | ✅ Supported — async export endpoint designed for this pattern |
|
||||
| Sandbox environment | ✅ Will be provided before production go-live |
|
||||
| Spray-area boundary polygons | ⏳ **Pending your confirmation (Section 4)** |
|
||||
@ -1,226 +0,0 @@
|
||||
General:
|
||||
What's good:
|
||||
+ User roles and permissions validation implemented in routes.
|
||||
+ Endpoint params are granularly validated using 'joi' schemas.
|
||||
+ Used helpers function for reusability and avoiding code duplication.
|
||||
|
||||
What's Todo for production:
|
||||
- DRY (DO NOT REPEAT YOURSELF) principle to be applied.
|
||||
- To avoid potential crashing, it should do guard check (i.e.: !utils.isEmptyArray(array)) before accessing/calling functions of any array, object, string etc. especially on function's input arguments.
|
||||
- Do not modify/restore to the orginal format if there are no behaviour changes, and keep the same formattings as it's easier for us to review the codes.
|
||||
- All ending code statements to be ended with comma (;).
|
||||
- 2 spaces (tab) each level indents to applied to all code files. When modifying existing files. The previous indentations should be kept intacted.
|
||||
- When done editing, a good practice to always performe:
|
||||
+ Pressing a combination of Ctrl+Shift+I to format the whole codes within the file to a common indentation. If using VSCode, these settings is already available under .vscode\settings.json file of each project folder.
|
||||
+ Clean up dead codes or redandant codes such as empty/unsed functions, imports, constants, etc.
|
||||
+ Add proper comments where there are complicated logic/algorithm.
|
||||
|
||||
- Keep simple codes (such as properties of objects) inline when possible for better readability.
|
||||
For example:
|
||||
const job = job.findOne({
|
||||
_id: jobId,
|
||||
});
|
||||
|
||||
Changed to: const job = job.findOne({ _id: jobId }); is much cleaner, shorter and easier to read.
|
||||
|
||||
------------------
|
||||
Routes:
|
||||
- Most of Joi fluent objects or child objects/properties are similar in structure. Can they be resused to avoid duplicated codes?
|
||||
- const editRoles, viewRoles should not be duplicated in all Job Invoicing routes.
|
||||
|
||||
invoice_settings.js:
|
||||
- Why paymentTerm in createInvoiceSettingSchema is required but optional in updateInvoiceSettingSchema?
|
||||
- .route('/byClient/:clientId') and .route('/:invoiceSettingId') are duplicated (repeated) with the exact similar handlings. How about using route regex be used or simply params 1 route? E.g.: .route('/:invoiceSettingId/byClient/:clientId')
|
||||
|
||||
costing_items.js:
|
||||
- Why all field validation for updateCostingItemSchema are optional compared but required in createCostingItemSchema?
|
||||
|
||||
invoices.js:
|
||||
- Most of properties and nested ones in .items({..}) in updateInvoiceSchema and in createInvoiceSchema are the same with only .code property different.
|
||||
- Why openDate, dueDate, jobs, clients are optional in updateInvoiceSchema but required in createInvoiceSchema?
|
||||
- Why companyName, address, logo, currency in createInvoiceSchema but not updateInvoiceSchema? Is currency optional in updateInvoiceSchema?
|
||||
- Why all costing item fields are optional in updateInvoiceSchema.jobs.items.costings? Are invalid costing items allowed?
|
||||
- What is clients.items.code for?
|
||||
- Is invoiceIds (array) expected in getExportMultipleInformationSchema intead of invoiceId?
|
||||
- route('/:id'). Validation schema is missing. To add validation for expected params.
|
||||
|
||||
log_payment.js:
|
||||
- Check and revise comments to refer to logpayment instead of '*invoice'
|
||||
- Why updateLogPaymentSchema is and empty Joi schema object (Joi.object({})) with no validation?
|
||||
- /createMultipleLogPayments => to renamed to /createLogPayments. Notice that it already ending with plural (s).
|
||||
- Input fields Naming should be consistent to avoid confusion. All object Ids to be [name]Id. E.g.: invoice => invoiceId, client => clientId. Please remember to update controller's handlers after these changes.
|
||||
|
||||
|
||||
------------------
|
||||
Controllers:
|
||||
- Errors are contrialized handled in /middlewares/error_handle.js, thus all try catch at the top level in each controllers's API handlers are redundant. Thus, they should be removed. Specific try/catch based on known logic can also be used then they might need to be rethrown.
|
||||
Example:
|
||||
async apiHandler(req, res, next) {
|
||||
try {
|
||||
....
|
||||
} catch (error) {
|
||||
AppParamError.throw(error);
|
||||
}
|
||||
}
|
||||
- Delete API handlers should not throw an error if the document(s) can not be found.
|
||||
- Naming. Controllers's API handler functions should be named ending with verb (_get, _post, _put) except delete.
|
||||
|
||||
invoice_settings.js:
|
||||
- Did the FrontEnd handle Errors.INVOICE_SETTING_NOT_FOUND when calling RUD Endpoints ?
|
||||
|
||||
costing_items.js:
|
||||
- In updateCostingItemSchema, why .name and .price are optional? When changing, please remember DRY.
|
||||
- Did the FrontEnd handle Errors.COSTING_ITEM_NOT_FOUND when calling RUD Endpoints ?
|
||||
|
||||
invoices.js:
|
||||
- To revise:
|
||||
+ Handler functions's naming convention (.._suffix exept delete). E.g.: getInvoices => getInvoices_get
|
||||
+ Possible inline codes
|
||||
|
||||
- getInvoices():
|
||||
+ To guard the the case if puid is null (or not an valid objectid) to avoid unnecessary whole collection scanning.
|
||||
+ To remove duplication codes for getting invoices and adding them to an array, line 34-76. Isn't there is only one field differed in the filter object?
|
||||
|
||||
- getJobById():
|
||||
+ if (!job) AppParamError.throw(Errors.TO_NOT_FOUND); => to correct the error code.
|
||||
|
||||
- generateRandomInvoiceCode():
|
||||
+ How about simplifing invoiceUnique by using utils.padZero(num, size) instead?
|
||||
|
||||
- createInvoice():
|
||||
+ What if invoiceBody.clients, invoiceBody.jobs, jobIds, listJobs are null or undefined (later)? => the whole server app would crash.
|
||||
+ To guard for the case if puid is null (or not an valid ObjectId). If puid is not valid, it must return [] or throw an error to avoid potential return all invoices within the db.
|
||||
|
||||
- getInvoiceById():
|
||||
+ To guard for the case if puid, invoiceId are null (or not an valid objectid).
|
||||
+ To avoid potential crashing, it should do guard check before accessing any array (e.g.: invoice.clients.filter()).
|
||||
+ The same duplication pattern (only diff. in query filter object) repeated when querying invoices, then checkInvoiceStatus() like mentioned in getInvoices(). To be revised avoiding code duplication.
|
||||
|
||||
- checkInvoiceStatus():
|
||||
+ What if invoice param is null or app? => undefined the whole server will crash.
|
||||
|
||||
- getPrintInformation():
|
||||
+ Line 223, invoice.clients.find(..) and line 240 invoiceObject.jobs(). To avoid potential crashing, it should do guard check before accessing any array.
|
||||
|
||||
- getUnitName(unit):
|
||||
+ Line 260, unit.toString().toLowerCase(). It should do safe guard check before calling unit's string chain functions to avoid potential crash.
|
||||
|
||||
- calInvoiceExport():
|
||||
+ As mentioned in getInvoiceById(). To guard for the case if puid is null (or not an valid ObjectId). If puid is not valid, it must return [] or throw an error to avoid potential return all invoices within the db.
|
||||
+ To revise and make sure always doing safe guard check before accessing an array (.clients, .jobs, .items,..)
|
||||
+ It returns an complicated data but the structure was not documented. To add documentation mentioning about the params and return data structure.
|
||||
|
||||
- geInvoiceCsv():
|
||||
+ 1st line, calInvoiceExport(req, invoiceIds)).flat().flat().flat(). Doing shortcuts to access array functions like this is not a good practice because of potential crash.
|
||||
+ Line 372, delimiter: ';'. Isn't it to be ',' for CSV output?
|
||||
|
||||
- splitAddress(inputAddress):
|
||||
+ inputAddress.trim(). What if inputAddress is null or undefined?
|
||||
|
||||
- geInvoiceIIF():
|
||||
+ 1st line, const invoices = await calInvoiceExport(req, invoiceIds); Is it expected to return an invoice list?. There is only 1 iteration for clients [line 419.. for (const clients of invoices)]
|
||||
|
||||
- getExportInformation(), getExportMultipleInformation():
|
||||
+ DRY. They looks almost 90+% duplicated. What is the real different in these two functions?
|
||||
+ Refactoring to only one function exportInvoices(req ({ invoiceIds:[], invoiceType }), res) should be enough to handle export 1 or multiple invoices.
|
||||
|
||||
- updateInvoiceById():
|
||||
+ Firstly, must do safe guard checks on input params (invoiceId (potential null or undefined), data (potential null of clients and jobs and others), puid) before using.
|
||||
+ When req.userInfo is null or undefined => It should not proceed but throw an AppError error.
|
||||
+ Less param objects => inline 1 line for readability.
|
||||
+ Invoice.update() is deprecated. To use other update[..] function instead.
|
||||
+ If arrays like .clients is optional, .clients.map will potentially crash.
|
||||
+ For effiency and data integrity, all updates logic (if status == Void) to be best made on a deep clone of the req.body then finally calling updateOne to the db at one time.
|
||||
|
||||
- deleteInvoiceById(), deleteInvoiceByIdUsecase():
|
||||
+ ..Invoice.findByIdAndDelete({ _id: invoiceId, byPuid: puid }). Model.findByIdAndDelete(id, ..) expect an single value id param not an condition object. To use Model.findOneAndDelete({_id, byPuid},..) instead.
|
||||
+ Thowing Errors.INVOICE_NOT_FOUND is not necessary.
|
||||
+ deleteInvoiceByIdUsecase() => rename to deleteInvoice(id, puid)
|
||||
+ These two function do the same thing with duplicated content. deleteInvoiceById() should call deleteInvoice(id, puid).
|
||||
|
||||
- deleteMultipleInvoices():
|
||||
+ To rename to deleteInvoices(..); calling deleteInvoice(id, puid).
|
||||
|
||||
- checkInvoiceStatus(invoice):
|
||||
+ First must do safe guard check on invoice before accessing its properties or the app will potentially crash if invoice is null or indefined.
|
||||
|
||||
|
||||
log_payment.js:
|
||||
- Why are these API handler functions blank? getLogPaymentById(req, res), updateLogPayment(req, res), deleteLogPayment(req, res)
|
||||
- createMultipleLogPayments() => to rename to createLogPayments as mentioned in the route.
|
||||
|
||||
- getLogPayments():
|
||||
+ The same duplication pattern (only diff. in query filter object) repeated when querying for payment logs. To remove duplication codes by using query condition conditionally.
|
||||
|
||||
- createLogPayment():
|
||||
+ Firstly, must do safe guard check on invoice.clients before calling invoice.clients.find() to avoid potential crash.
|
||||
+ Why saving the invoice without any changes? Line 74, invoice.save()
|
||||
|
||||
- createMultipleLogPayments():
|
||||
+ createMultipleLogPayments => renamed to createLogPayments as mentioned in route.
|
||||
+ Why also saving invoice here? Line 122, await invoice.save()
|
||||
+ Why throw Errors.TO_NOT_FOUND error (line 104)?
|
||||
|
||||
- createLogPayment() and createMultipleLogPayments():
|
||||
+ Both have the same duplicated logic to log an payment log. To refactor to one function called LogPayment(paymentLog). Then reuse them in these two handler functions.
|
||||
|
||||
job.js:
|
||||
- handleCostingItems().... Why not use the costing items as being seen on the UI when the user creating/updating a job? Please remove this logic.
|
||||
|
||||
- createJob_post():
|
||||
+ In the UI, it does not clone the job's costing items. Why calling handleCostingItems() here?
|
||||
+ As Why does it still query the invoices collection to get invoice's related info? This would create an future severve performance issue when there are lots of invoices. How's about adding an invoiceStatus field within the job model and updating the corresponding status when needed? Remember to update the status again when a job was removed from and invoice or when the invoice is deleted.
|
||||
|
||||
- updateJob_put():
|
||||
+ Why querying and updating invoices, jobs's costing items when updating a job? It is unnecessary and will create a problem of user data integrity and performance hit.
|
||||
|
||||
- getJobs_get():
|
||||
+ When added the invoiceStatus to the job, the join/lookup to invoices collection can be removed here. Then, It would help to improve the performance by avoid scanning invoices collection.
|
||||
|
||||
|
||||
------------------
|
||||
Models:
|
||||
- To simplify properties with inline codes for cleaner and readability.
|
||||
|
||||
costing_item.js:
|
||||
- userParentId field to be renamed to byPuid for consistency.
|
||||
|
||||
invoice_settings.js:
|
||||
- Please add comments for .userId and userParenetId. Is .userId for storing a client userid?
|
||||
|
||||
invoice.js:
|
||||
- PaymentTerm should not be confused with real life invoice payment term (days to due(be paid) after open). Thus rename to something like createOp to represent how the invoice was created with one of the three options. Likewise, the PaymentTerm constant to be rename as InvCreateOption
|
||||
- overdueDateAllowance. Should be renamed to paymentTerm.
|
||||
|
||||
location.js:
|
||||
+ What is require('./invoice_settings'); for?
|
||||
|
||||
job.js:
|
||||
+ add invoiceStatus field to manage its invoicing status as mentioned in the job.js controller.
|
||||
+ Please restore the original spacing and indents
|
||||
|
||||
------------------
|
||||
Helpers:
|
||||
- We use global dotenv to avoid duplication in each project and for cleaner deployment. Thus, no dotenv in local dependencies. Reference: Preload in https://www.npmjs.com/package/dotenv
|
||||
|
||||
validate.js:
|
||||
- To be relocated under /middlewares/ folder.
|
||||
- HTTP 400 status code and the validation error message. How/did the FrontEnd handle these Joi validation errors?. We will need to merge this into the application current architech by returning an AppInputError with 409 code and may be along with the additional message field.
|
||||
|
||||
multer.js:
|
||||
+ folderInvoiceSettingUpload => hidden path constant. It is to use an ENV param such as INV_CFG_UPLOAD_DIR. Please be noted that all ENV params need to be wrapped in the env.js helper module.
|
||||
+ To be relocated under /middlewares/ folder.
|
||||
|
||||
- invoiceSettingsUpload():
|
||||
+ It should allow image file extentions in uppercase as well.
|
||||
+ fileSize should use an ENV like INV_CFG_MAX_UPLOAD_SIZE_MB = 5 (default).
|
||||
|
||||
env.js:
|
||||
+ We are using global dotenv. Please remove dotenv related usages.
|
||||
|
||||
utils.js
|
||||
+ Why using Bignumber types?
|
||||
+ Why using dayjs? momentjs has been used and should be used in the project.
|
||||
|
||||
------------------
|
||||
|
||||
@ -1,246 +0,0 @@
|
||||
General:
|
||||
What's Todo for production:
|
||||
- DRY (DO NOT REPEAT YOURSELF) principle to be applied.
|
||||
- To avoid potential crashing, it should do guard check (i.e.: !utils.isEmptyArray(array)) before accessing/calling functions of any array, object, string etc. especially on function's input arguments.
|
||||
- Do not modify/restore to the orginal format if there are no behaviour changes, and keep the same formattings as it's easier for us to review the codes.
|
||||
- All ending code statements to be ended with comma (;).
|
||||
- 2 spaces (tab) each level indents to applied to all code files. When modifying existing files. The previous indentations should be kept intacted.
|
||||
- When done editing, a good practice to always performe:
|
||||
+ Pressing a combination of Ctrl+Shift+I to format the whole codes within the file to a common indentation. If using VSCode, these settings is already available under .vscode\settings.json file of each project folder.
|
||||
+ Clean up dead codes or redandant codes such as empty/unsed functions, imports, constants, etc.
|
||||
+ Add proper comments where there are complicated logic/algorithm.
|
||||
|
||||
- Error Handling: We suggest to centralize error handling using following pattern:
|
||||
1. create a util function that accepts an error object and returns a message string
|
||||
function handleErr(error): string {
|
||||
// extract error code
|
||||
// if error code has message attached return error message,
|
||||
// else map error code with a message
|
||||
return 'error message'
|
||||
}
|
||||
2. use the handleErr function inside the error block of any observable subscriptions
|
||||
3. display the message to the user either inline or with the msgSvc like
|
||||
this.invoiceSvc.getSettingByClientId().subscribe({error : (err) => this.handleErr.msgSvc(addFailedMsg(err))})
|
||||
|
||||
----------------------------
|
||||
customer-settings.component.ts
|
||||
invoice-settings-guard.service.ts
|
||||
settings.component.ts
|
||||
customer-settings-resolver.service.ts
|
||||
setting-resolver.service.ts
|
||||
|
||||
- The spacing format have been modified in serveral contents from the original without any behaviour changes in the following files:
|
||||
job-edit.component.html
|
||||
job-edit.component.ts
|
||||
job-list.component.ts
|
||||
app.menu.components.ts
|
||||
- Example:
|
||||
<input type="text" id="orderNum" name="orderNum" pInputText [(ngModel)]="selectedItem.orderNumber" maxlength="20" style="font-weight: bold;">
|
||||
have been changed to
|
||||
<input type="text" id="orderNum" name="orderNum" pInputText [(ngModel)]="selectedItem.orderNumber"
|
||||
maxlength="20" style="font-weight: bold;">
|
||||
|
||||
+ Do not modify (if did please restore to) the orginal format if there are no behaviour changes, and keep the same formattings as it's easier for us to review the codes.
|
||||
|
||||
- Checking for not null of a service is not necessary since the base component should already injected it.
|
||||
Example:
|
||||
this.authSvc != null && this.authSvc.hasRole([RoleIds.APP, RoleIds.APP_ADM]);
|
||||
|
||||
- The the business logic to calculate the values for (subTotal, discounted, totalExcludingTax, taxed, total) are duplicated in
|
||||
invoice-detail.component.ts
|
||||
invoice-edit.component.ts
|
||||
invoices-list.component.ts
|
||||
- These calculations are critical to the application and should be placed in a single util function, and tested for various of edge cases.
|
||||
+ The generic format of the function is something like:
|
||||
calcInvoice(subTotal, discount, taxRate) {
|
||||
// do calculation
|
||||
return { subTotal, discounted, totalExcludingTax, taxed, total };
|
||||
}
|
||||
|
||||
- The calculation for the sum of an array is redundant in many components.
|
||||
|
||||
job-list.component.ts
|
||||
Example:
|
||||
this.invoice.clients
|
||||
.map(i => this.billToListPriceObject(i).totalExcludingTax)
|
||||
.reduce(
|
||||
(accumulator, currentValue) => accumulator + currentValue, 0,
|
||||
);
|
||||
|
||||
this.invoice.clients
|
||||
.map(i => +i.split)
|
||||
.reduce(
|
||||
(accumulator, currentValue) => accumulator + currentValue, 0,
|
||||
);
|
||||
Do:
|
||||
- Replace all these redundant sums with a common util function that return a default value in case the array is not defined. The structure of the util function can be something like:
|
||||
const sum = (arr: Array[], field: string, func?: Function) => arr?.map((item) => func ? +(func(item)[field]) : +item[field]).reduce((acc,val) => acc + val, 0) || 0;
|
||||
totalExcludingTax = sum(this.invoice.clients, 'totalExcludingTax', this.billToListPriceObject);
|
||||
totalSplit = sum(this.invoice.clients, 'split');
|
||||
|
||||
- Make sure the min and max values of form fields are bounded according to context
|
||||
Example:
|
||||
- Taxes and Discount percentages should be bounded min = 0, max = 100
|
||||
|
||||
- Various http calls to backend api that being done directly within these components:
|
||||
invoice-detail.component.ts
|
||||
invoice-edit.component.ts
|
||||
invoices-list.component.ts
|
||||
- Some of these functions have usability issues that may effect the user like downloadCSV, saveWithLogPayment, saveWithLogPayment. These calls don't have proper error handlings
|
||||
incase if the http calls return error conditions.
|
||||
Do:
|
||||
- Separate these calls into ngrx effects and dispatch them with proper error handlings or add error handling for each subscribe block.
|
||||
|
||||
invoice-edit.component.html
|
||||
- Don't linebreak on translated text as it will add additional line when the text is translated to another language.
|
||||
Example:
|
||||
<span i18n="@@amountReqVal" *ngIf="logPaymentForm.amount == 0 && (amount.dirty || amount.touched)"
|
||||
class="ui-message ui-messages-error ui-corner-all">
|
||||
Amount is required
|
||||
</span>
|
||||
should change to:
|
||||
<span i18n="@@amountReqVal" *ngIf="logPaymentForm.amount == 0 && (amount.dirty || amount.touched)" class="ui-message ui-messages-error ui-corner-all">Amount is required</span>
|
||||
|
||||
- Try to separate translated text from special characters like (:,%,!).
|
||||
Example:
|
||||
<label i18n="@@taxRate">Tax rate (%)</label>
|
||||
should change to:
|
||||
<label><ng-container i18n="@@taxRate">Tax rate</ng-container> (%)</label>
|
||||
|
||||
- Create css class for the rules
|
||||
"background-color: transparent; color: #212121;"
|
||||
"text-align: center; font-weight: 600; border-bottom: 1px solid #bdbdbd;"
|
||||
"gap: 4px; justify-content: center"
|
||||
"justify-content: end"
|
||||
"border-bottom: 1px solid #bdbdbd;"
|
||||
"display: flex; justify-content: space-between"
|
||||
Do:
|
||||
- Use css overriding to reuse rules
|
||||
|
||||
- The logic isNew || isDraftEdit is repeated in many places in the template and the ts files
|
||||
Do:
|
||||
- Create a predicate function for the logic isNew || isDraftEdit
|
||||
|
||||
invoice-edit.component.ts
|
||||
- getSettingByClientId http call on line 369-396, why is there such complex error handling mechanism, and it is being called again on line 773
|
||||
and the error handling here is different.
|
||||
Do:
|
||||
- If getSettingByClientId information is reused, use a resolver to fetch the data before the component is loaded. Otherwise, if this information is
|
||||
critical to the component, show a descriptive errror message to the user explaining why. As of right now, it seems that it just fails silently and resets the form to some values.
|
||||
|
||||
- There are no error handlings of the observable subscriptions in the function saveNewTerm and removeTermOpt, loadJobs, fetchLogPayment, saveInvoice.
|
||||
Do:
|
||||
- Generally any function that make http request should have error handling condition with a message telling the source of the error.
|
||||
|
||||
- In saveWithLogPayment, removeBillToClient, addClientToInvoice, saveNewTerm, addClientDlg make sure that
|
||||
this.selectedClients, and this.selectedJobs, this.paymentTermOpts this.orgSearchClientList are not null before calling map and [...] syntax
|
||||
Do:
|
||||
- Use optional chaining https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining or if statement to check for null arrays first
|
||||
|
||||
- saveWithLogPayment, saveNewTerm, saveLog the asynchronous call sequence implementation are incorrect without proper error handlings.
|
||||
Do:
|
||||
- Use pipe to chain the http calls. For example,
|
||||
this.invoiceSvc.saveInvoice().pipe(
|
||||
switchMap(() => {
|
||||
return this.invoiceSvc.createListLogPayment(payload);
|
||||
})
|
||||
).subscribe({error: (err) => handleErr(err)}); // Generic handleErr() mentioned above in General.
|
||||
|
||||
if these same sequence of calls are repeated, create an ngrx action and effect for the sequence and dispatch it with a single command.
|
||||
see https://rxjs.dev/api/index/function/pipe on how to chain the async calls and catch the errors in one place.
|
||||
|
||||
invoices-list.component.ts
|
||||
- On line 147-158 ngAfterViewInit, check dueDate.value and dueDate.value for null before calling map function. What happen when the user clear the calendar? is there a function that clears 'inv-ops' from sessionStorage?
|
||||
|
||||
invoice-detail.component.html
|
||||
- See invoice-edit.component comments for translated text line breaks and special characters.
|
||||
|
||||
- Create css class for rules
|
||||
"width: 200px; text-align: right; font-weight: 400;"
|
||||
"border-bottom: 1px solid #bdbdbd;"
|
||||
"font-weight: 600;"
|
||||
"min-width: 150px; padding-left: 0;"
|
||||
"padding-left: 0;"
|
||||
|
||||
- Why is there an interpolation of a digit ({{1}}) value between lines 365 and 369 in invoice-detail.component.html
|
||||
|
||||
- Separate translated text from nested elements with interpolation:
|
||||
Example:
|
||||
<div i18n="@@tax" style="width: 150px; text-align: right; flex: 1;">Tax
|
||||
<span>({{printDetail.client.taxRate}}%)</span>
|
||||
</div>
|
||||
change to
|
||||
<div style="width: 150px; text-align: right; flex: 1;">
|
||||
<ng-container i18n="@@tax">Tax</ng-container>
|
||||
<span>({{printDetail.client.taxRate}}%)</span>
|
||||
</div>
|
||||
|
||||
- Create a function to compute the value as these are redundant.
|
||||
printDetail.client.amountDue ? printDetail.client.amountDue : printDetail.total
|
||||
|
||||
invoice-detail.component.ts
|
||||
- In the saveLog function of the observable subscriptions between lines 316-327, do these async api calls need to be sequential?
|
||||
- what if fetchInvoiceDetail, fetchLogPayment failed? the message snackbar will still show "Create log payment succeeded" as it will be confusing to the user.
|
||||
Do:
|
||||
- See https://rxjs.dev/api/index/function/pipe on how chain the async calls and catch the errors in one place.
|
||||
|
||||
settings.component.html
|
||||
- See invoice-edit.component.html comments for translated text line breaks and special characters.
|
||||
- See invoice-edit.component.html comments for separating translated text from nested elements.
|
||||
- Interpolate the allowedFormats array items for the allowed formats text on line (134-138) as these can change later on.
|
||||
- Max value for Tax and Discount % should be 100.
|
||||
|
||||
settings.component.ts
|
||||
- Move allowedFormats, maxLogoSize constant into a shared variable in the module.
|
||||
- Create a requiredFieldTrimmed function in invoice service to share the logic in both setting.component and customer-setting.component.
|
||||
Example:
|
||||
requiredFieldTrimmed(setting: CustomerInvoiceSetting) {
|
||||
return setting?.companyName?.length == setting?.companyName?.trim().length && setting?.address?.length == setting?.address?.trim().length
|
||||
}
|
||||
- There are no error handling in the functions saveNewTerm,removeTermOpt see comments for invoice-edit.component.ts above for more details on how to handle http call errors.
|
||||
|
||||
customer-settings.component.html
|
||||
- See invoice-edit.component comments for translated text line breaks and special characters.
|
||||
- See settings.component comments for allowedFormats, maxLogoSize, requiredFieldTrimmed.
|
||||
- Max value for Tax and Discount % should be 100.
|
||||
|
||||
costing-item.component.html
|
||||
- Wrap translatable texts in p-header element with ng-container.
|
||||
|
||||
job-list.component.ts
|
||||
- Create an enum constant for statuses ('all', 'new', 'ready', 'download', 'spray', 'invoiced').
|
||||
- Wrap lines 148-168 into a function and do a return after each this.statusFilter assignment avoiding future bugs where more than one of the conditions can be true.
|
||||
Example:
|
||||
if (!listFilter.filters.status?.value && listFilter.filters.invoices?.value == 1) {
|
||||
this.statusFilter = 'invoiced';
|
||||
}
|
||||
change to
|
||||
if (!listFilter.filters.status?.value && listFilter.filters.invoices?.value == 1) {
|
||||
return this.statusFilter = 'invoiced';
|
||||
}
|
||||
- Is there a default value for this.statusFilter if (listFilter && listFilter.filters) is false?
|
||||
- Should this.statusFilter = 'all' be in the the outter if/else as the default value instead?
|
||||
|
||||
job-edit.component.ts
|
||||
- Why is there async await in createInvoice function?
|
||||
|
||||
invoice-list-canactive.guard.ts
|
||||
- What is the purpose of this guard? its only function is to fetch the invoice list. The list then get fetched again in invoices-list.component.ts ngOnInit.
|
||||
|
||||
invoice-edit-resolver.service.ts and invoice-detail-resolver.service.ts
|
||||
- These two resolvers get the exact same data from the backend. Try to combine them into one resolver.
|
||||
|
||||
invoice-create-canactive.guard.ts and customer-setting-list-canactive.guard.ts
|
||||
- These two guards seem to have the same function. Try to combine them into one.
|
||||
|
||||
enums folder contents
|
||||
- Combine these ts files into a single shared file in the module as these are just constant values.
|
||||
- Freeze the invoiceStatus object.
|
||||
|
||||
invoice.service.ts
|
||||
- getPrintDetail need a return type from the backend.
|
||||
- Check for null array of jobs parameter in flattenJobCostingItem(jobs).
|
||||
|
||||
client.service.ts
|
||||
- getClientWithInvoiceSetting needs a return type (important: API contract) from the backend.
|
||||
|
||||
@ -1,49 +0,0 @@
|
||||
** Back navigation: Need to have better control on the flow back and forth started from Services <-> Billing Address <-> Payment Method, ...**
|
||||
Expectation: restoring selected items or entered values in fields, the flow back and forth must be in the right order (base on the steps the user made).
|
||||
|
||||
** New Route issues: on some screens, Job List, Job Detail, Job Map, Upload Job Data, etc.. Refresh (F5) will show the green screen with AgNav logo in the center, F5 again will lost the current screen and show Dashboard (root) screen. **
|
||||
Expectation: should reload the screen at the current path with selected item, if any.
|
||||
|
||||
Billing Address:
|
||||
1. When loaded, it always filled with a fixed name, 'justin'? Why?
|
||||
Expectation:
|
||||
When loaded, it should filled with:
|
||||
- Name: the contact name of the Applicator as default or the previous input Name
|
||||
- Address fields: if there is none BA, extract the fields from address (normally would be the exact business address registered with us) of the appl. profile.
|
||||
|
||||
2. When try to change the country, it emits error. However, it reset values of state/province and postal code.
|
||||
Expectation:
|
||||
- Can country be fixed and or not changable? The reason is the BA make senses only for the country the applicator's business location which is mostly within the same province/state.
|
||||
|
||||
Q:
|
||||
- Sometimes it shows 'Error: Received unknown parameter: billing_details[address][valid] ?' at the bottom of the screen Why? Is the a better to handle this?
|
||||
- If the Stripe Address element is hard to use and how about simply implement a reuse-able Address Component ?
|
||||
|
||||
Services:
|
||||
When selected a package then Confirm => Billing Address. If click on Back, it goes back to the Services with none selected. => Need to display previous selected services.
|
||||
Expectation:
|
||||
- At any time, when navigating back to, It should show the previous selected items.
|
||||
|
||||
Payment Details:
|
||||
1. Total Amount: shows plain number only, the value is incorrect.
|
||||
Expectation:
|
||||
- Show value in US$: <value> or with the suffix of USD next to the value.
|
||||
- The value number must be divided by 100, if getting from Stripe, and be formatted with US 2-decimal precision.
|
||||
|
||||
2. 'Choose Existing Payment' and the dropdown with 'None'.
|
||||
Expectation:
|
||||
- When there is none PM, hide/disable 'Choose Existing Payment' and the dropdown with 'None'.
|
||||
|
||||
3. 'Card Number':
|
||||
Expectation: auto focus on the 'Card Number' field
|
||||
|
||||
4. When enter a well-form card number such as 4242 4242 4242 4245, it displays 'secret red error icon', What does it mean to the user? How does he know how to proceed and why?
|
||||
Expectation:
|
||||
- Should show clear and meaningly error, may be right below the CN field, so the user can correct.
|
||||
|
||||
5. Errors: Received unknown parameter: billing_details[address][valid] when click on 'Continue to review'
|
||||
Expectation:
|
||||
- For the case of invalid customer address (unverified from Stripe), as mentioned before, It should show a clear warning message and a button to click on to go back to the Billing Address screen. Only proceed until the Address is valid and verified by Stripe.
|
||||
- Or if any error, check for the reason and and the request defails from the Debug and Stripe. For example: for this error, Response: "invalid_request_error", https://dashboard.stripe.com/test/logs/req_gWrKrTzQEHpMxI?t=1683291978
|
||||
|
||||
|
||||
@ -1,407 +0,0 @@
|
||||
# SVN Guidelines — AgMission Project
|
||||
|
||||
> Reference for daily SVN workflow, branching, merging, and common commands.
|
||||
> Repo root: `https://svn.agnav.com:8443/svn/AgMission`
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
1. [Repository Layout](#sec-1)
|
||||
2. [Daily Workflow](#sec-2)
|
||||
3. [Branching](#sec-3)
|
||||
4. [Keeping a Branch in Sync with Trunk](#sec-4)
|
||||
5. [Merging a Branch into Trunk (Reintegrate)](#sec-5)
|
||||
6. [After Reintegrate: Branch Lifecycle](#sec-6)
|
||||
7. [Resolving Conflicts](#sec-7)
|
||||
8. [Common Commands Reference](#sec-8)
|
||||
9. [Best Practices and What to Avoid](#sec-9)
|
||||
|
||||
---
|
||||
|
||||
<a id="sec-1"></a>
|
||||
## 1. Repository Layout
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
ROOT["AgMission/ — repo root"]
|
||||
ROOT --> T["trunk/"]
|
||||
ROOT --> B["branches/"]
|
||||
ROOT --> G["tags/"]
|
||||
T --> DEV["Development/<br/>stable · always deployable"]
|
||||
B --> SR["satloc-resume/<br/>spent — reintegrated"]
|
||||
B --> DEA["data-export-api/<br/>active feature branch"]
|
||||
B --> JI["job-invoicing/<br/>active feature branch"]
|
||||
G --> R321["release-3.2.1/<br/>immutable snapshot"]
|
||||
|
||||
style DEV fill:#c8e6c9,stroke:#388e3c
|
||||
style SR fill:#ffcdd2,stroke:#c62828
|
||||
style DEA fill:#fff9c4,stroke:#f9a825
|
||||
style JI fill:#fff9c4,stroke:#f9a825
|
||||
style R321 fill:#e1bee7,stroke:#7b1fa2
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- `trunk/Development` is always deployable. Never commit broken code directly here.
|
||||
- Each significant feature or release cycle gets its own branch.
|
||||
- Tags are read-only snapshots — never commit to a tag.
|
||||
|
||||
---
|
||||
|
||||
<a id="sec-2"></a>
|
||||
## 2. Daily Workflow
|
||||
|
||||
### Start of day — update your working copy
|
||||
|
||||
```bash
|
||||
# Update the branch you're actively working in
|
||||
cd /path/to/AgMission/branches/data-export-api
|
||||
svn update
|
||||
|
||||
# Also update trunk if you need to reference it
|
||||
cd /path/to/AgMission/trunk/Development
|
||||
svn update
|
||||
```
|
||||
|
||||
### Check what you've changed
|
||||
|
||||
```bash
|
||||
svn status # show all local changes (M=modified, A=added, D=deleted, ?=unversioned)
|
||||
svn status -q # quiet: versioned changes only (excludes ? files)
|
||||
svn diff # full diff of all changes
|
||||
svn diff path/to/file # diff of a specific file
|
||||
```
|
||||
|
||||
### Stage and commit
|
||||
|
||||
SVN has no staging area — `svn commit` sends everything marked for change.
|
||||
|
||||
```bash
|
||||
# Commit all pending changes
|
||||
svn commit -m "Meaningful commit message describing WHAT and WHY"
|
||||
|
||||
# Commit specific files only
|
||||
svn commit file1.js file2.js -m "Fix X in file1 and Y in file2"
|
||||
```
|
||||
|
||||
**Commit message format (recommended):**
|
||||
```
|
||||
#<ticket> Short summary (imperative mood)
|
||||
|
||||
- Detail line 1
|
||||
- Detail line 2
|
||||
```
|
||||
|
||||
### End of day — make sure nothing is left uncommitted
|
||||
|
||||
```bash
|
||||
svn status -q # should return nothing if all work is committed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a id="sec-3"></a>
|
||||
## 3. Branching
|
||||
|
||||
### Create a new feature branch from trunk
|
||||
|
||||
```bash
|
||||
# Server-side copy — instant, no data transferred
|
||||
svn copy ^/trunk/Development ^/branches/my-feature \
|
||||
-m "Create my-feature branch from trunk rXXX"
|
||||
|
||||
# Then check it out locally (if your working copy doesn't cover /branches/)
|
||||
svn update branches/my-feature
|
||||
```
|
||||
|
||||
### Create a release tag
|
||||
|
||||
```bash
|
||||
svn copy ^/trunk/Development ^/tags/release-X.Y.Z \
|
||||
-m "Tag release X.Y.Z"
|
||||
```
|
||||
|
||||
### Delete a branch (after reintegrate — see §6)
|
||||
|
||||
```bash
|
||||
svn delete ^/branches/my-feature \
|
||||
-m "Remove spent my-feature branch post-reintegrate"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a id="sec-4"></a>
|
||||
## 4. Keeping a Branch in Sync with Trunk
|
||||
|
||||
**Do this regularly** (at least weekly, or whenever trunk has significant commits) to keep the
|
||||
branch up to date and avoid large conflict batches at reintegrate time.
|
||||
|
||||
```bash
|
||||
# From inside your feature branch working copy
|
||||
cd branches/my-feature
|
||||
svn update # make sure WC is current
|
||||
svn merge ^/trunk/Development # merge trunk changes into branch
|
||||
# resolve any conflicts (see §7)
|
||||
svn commit -m "Sync trunk rXXX into my-feature"
|
||||
```
|
||||
|
||||
> ⚠️ **Never use `--record-only` for a routine sync.** `--record-only` skips applying file
|
||||
> changes and only updates mergeinfo. Use it only to fix mergeinfo bookkeeping gaps caused by
|
||||
> very old revisions that predate the branch and have no content relevance. Using it incorrectly
|
||||
> will silently discard real changes from trunk.
|
||||
|
||||
---
|
||||
|
||||
<a id="sec-5"></a>
|
||||
## 5. Merging a Branch into Trunk (Reintegrate)
|
||||
|
||||
SVN 1.8+ detects reintegration automatically. You do **not** need `--reintegrate`
|
||||
(deprecated in 1.8, no-op in 1.14).
|
||||
|
||||
### Prerequisites
|
||||
- The branch must have been **fully synced** with trunk (§4) and that sync committed.
|
||||
- Your trunk working copy must be clean (`svn status -q` returns nothing).
|
||||
- No local modifications in trunk WC.
|
||||
|
||||
```bash
|
||||
# 1. Sync branch one last time (if there are recent trunk commits)
|
||||
cd branches/my-feature
|
||||
svn update
|
||||
svn merge ^/trunk/Development
|
||||
svn commit -m "Final trunk sync before reintegrate"
|
||||
|
||||
# 2. Switch to trunk and update
|
||||
cd ../../trunk/Development
|
||||
svn update
|
||||
|
||||
# 3. Run the reintegrate merge
|
||||
svn merge ^/branches/my-feature
|
||||
# SVN auto-detects this as reintegrate direction (branch → trunk)
|
||||
|
||||
# 4. Review and resolve any conflicts (see §7)
|
||||
svn status
|
||||
svn diff
|
||||
|
||||
# 5. Syntax-check JS files if applicable
|
||||
node --check server/path/to/changed.js
|
||||
|
||||
# 6. Commit the merge
|
||||
svn commit -m "Merge my-feature into trunk (reintegrate r1000-rXXX)"
|
||||
```
|
||||
|
||||
### What a reintegrate does
|
||||
|
||||
```mermaid
|
||||
%%{init: {'gitGraph': {'mainBranchName': 'trunk', 'rotateCommitLabel': false}}}%%
|
||||
gitGraph
|
||||
commit id: "r999 — branch point"
|
||||
branch my-feature
|
||||
checkout my-feature
|
||||
commit id: "r1001 feature work"
|
||||
commit id: "r1002 feature work"
|
||||
checkout trunk
|
||||
commit id: "r1003 trunk work"
|
||||
checkout my-feature
|
||||
merge trunk id: "r1010 sync from trunk"
|
||||
commit id: "r1050 final feature commit"
|
||||
checkout trunk
|
||||
merge my-feature id: "r1051 reintegrate"
|
||||
```
|
||||
|
||||
SVN applies only the revisions from the branch that haven't already been merged to trunk
|
||||
(tracked via `svn:mergeinfo`). After the reintegrate commit, the branch is **spent** — see §6.
|
||||
|
||||
---
|
||||
|
||||
<a id="sec-6"></a>
|
||||
## 6. After Reintegrate: Branch Lifecycle
|
||||
|
||||
A reintegrated branch is **spent** — its mergeinfo record says "all revisions merged." Do not
|
||||
continue developing on it; create a fresh branch from trunk instead.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
direction LR
|
||||
[*] --> Active : svn copy from trunk
|
||||
Active --> Active : commit feature work
|
||||
Active --> Syncing : svn merge trunk (weekly)
|
||||
Syncing --> Active : conflicts resolved + committed
|
||||
Active --> Reintegrating : feature complete
|
||||
Reintegrating --> Spent : merge committed to trunk
|
||||
Spent --> [*] : tag + svn delete
|
||||
|
||||
note right of Syncing
|
||||
Never use --record-only
|
||||
for routine syncs
|
||||
end note
|
||||
note right of Spent
|
||||
Always tag before
|
||||
deleting the branch
|
||||
end note
|
||||
```
|
||||
|
||||
```bash
|
||||
# Optional: tag the branch state before deleting
|
||||
svn copy ^/branches/my-feature ^/tags/my-feature-merged-rXXX \
|
||||
-m "Tag my-feature before deletion"
|
||||
|
||||
# Delete the branch from the repository
|
||||
svn delete ^/branches/my-feature \
|
||||
-m "Remove spent my-feature branch post-reintegrate (rXXX)"
|
||||
|
||||
# Remove it from your local working copy
|
||||
svn update # this will remove the local branches/my-feature directory
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a id="sec-7"></a>
|
||||
## 7. Resolving Conflicts
|
||||
|
||||
### Listing conflicts
|
||||
|
||||
```bash
|
||||
svn status | grep "^C\|^.C" # text conflicts (C) and tree conflicts
|
||||
```
|
||||
|
||||
### Text conflict — manual resolution
|
||||
|
||||
```bash
|
||||
# Edit the file and fix conflict markers (<<<<, ====, >>>>)
|
||||
# Then mark as resolved
|
||||
svn resolve --accept working path/to/file.js
|
||||
```
|
||||
|
||||
### Text conflict — accept one side wholesale
|
||||
|
||||
```bash
|
||||
svn resolve --accept mine-full path/to/file # keep your local version
|
||||
svn resolve --accept theirs-full path/to/file # take incoming version entirely
|
||||
```
|
||||
|
||||
### Tree conflict (directory added/deleted/replaced)
|
||||
|
||||
```bash
|
||||
svn info path/to/dir # read the conflict description
|
||||
# After deciding what to keep:
|
||||
svn resolve --accept working path/to/dir # keep local (working copy) state
|
||||
```
|
||||
|
||||
### After resolving all conflicts
|
||||
|
||||
```bash
|
||||
svn status | grep "^C" # should return nothing
|
||||
svn commit -m "Resolve merge conflicts"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a id="sec-8"></a>
|
||||
## 8. Common Commands Reference
|
||||
|
||||
### Information
|
||||
|
||||
| Command | What it does |
|
||||
|---|---|
|
||||
| `svn info` | Show WC path, URL, revision, last-changed info |
|
||||
| `svn info ^/branches/my-feature` | Info on a repo path (no checkout needed) |
|
||||
| `svn log --limit 10` | Last 10 commits on current path |
|
||||
| `svn log ^/trunk/Development --limit 5` | Last 5 commits on trunk |
|
||||
| `svn log -v --limit 3` | Commits with list of changed files |
|
||||
| `svn blame file.js` | Show author/revision per line |
|
||||
|
||||
### Status & diff
|
||||
|
||||
| Command | What it does |
|
||||
|---|---|
|
||||
| `svn status` | All local changes (M, A, D, ?, C) |
|
||||
| `svn status -q` | Versioned changes only |
|
||||
| `svn diff` | Full diff of local changes |
|
||||
| `svn diff -r HEAD` | Diff against HEAD revision |
|
||||
| `svn diff ^/trunk/Development ^/branches/my-feature` | Diff two repo paths |
|
||||
|
||||
### Update & revert
|
||||
|
||||
| Command | What it does |
|
||||
|---|---|
|
||||
| `svn update` | Bring WC up to latest revision |
|
||||
| `svn update -r 993` | Update to a specific revision |
|
||||
| `svn revert file.js` | Discard local changes to one file |
|
||||
| `svn revert -R .` | Discard ALL local changes recursively |
|
||||
|
||||
### Add / remove
|
||||
|
||||
| Command | What it does |
|
||||
|---|---|
|
||||
| `svn add newfile.js` | Schedule new file for versioning |
|
||||
| `svn add --force new-dir/` | Add entire new directory |
|
||||
| `svn delete file.js` | Schedule file for deletion |
|
||||
| `svn delete ^/branches/old-branch -m "msg"` | Delete repo path directly |
|
||||
|
||||
### Merge
|
||||
|
||||
| Command | What it does |
|
||||
|---|---|
|
||||
| `svn merge ^/trunk/Development` | Sync trunk into current branch (run from branch WC) |
|
||||
| `svn merge ^/branches/my-feature` | Reintegrate branch into trunk (run from trunk WC) |
|
||||
| `svn merge ^/trunk/Development --dry-run` | Preview merge without applying |
|
||||
| `svn merge --record-only ^/trunk/Development` | Fix mergeinfo gaps only — no file changes |
|
||||
| `svn mergeinfo ^/branches/my-feature` | Show what has/hasn't been merged |
|
||||
|
||||
### Copy / branch / tag
|
||||
|
||||
| Command | What it does |
|
||||
|---|---|
|
||||
| `svn copy ^/trunk/Development ^/branches/new -m "msg"` | Create branch |
|
||||
| `svn copy ^/trunk/Development ^/tags/v1.0 -m "msg"` | Create tag |
|
||||
| `svn copy ^/branches/old ^/branches/new -m "msg"` | Copy branch |
|
||||
|
||||
---
|
||||
|
||||
<a id="sec-9"></a>
|
||||
## 9. Best Practices and What to Avoid
|
||||
|
||||
### ✅ Do
|
||||
|
||||
- **Commit often** with small, focused commits and meaningful messages.
|
||||
- **Sync trunk into your branch regularly** (`svn merge ^/trunk/Development`) — at least weekly.
|
||||
Early, small syncs are far easier to resolve than one big sync at reintegrate time.
|
||||
- **Run `svn update` before every commit** to catch conflicts early.
|
||||
- **Syntax-check JS files** before committing: `node --check path/to/file.js`
|
||||
- **Use `--dry-run`** on any merge to preview the result before applying.
|
||||
- **Tag releases** before merging or deleting branches.
|
||||
- **Delete spent branches** to keep the branch list clean.
|
||||
- **Keep unversioned files out of the WC** — use `.svnignore` or the global `global-ignores` setting.
|
||||
|
||||
### ❌ Avoid
|
||||
|
||||
| Mistake | Why | Instead |
|
||||
|---|---|---|
|
||||
| Committing directly to trunk | Breaks other developers; trunk must stay stable | Work on a feature branch |
|
||||
| `--record-only` on a routine sync | Silently skips real trunk changes → content drift | Only use to fix bookkeeping gaps on very old revisions |
|
||||
| Letting a branch go unsynced for months | Massive conflict blast at reintegrate | Sync from trunk weekly |
|
||||
| Continuing to develop on a spent (reintegrated) branch | SVN mergeinfo considers all revisions merged; new commits will be missed on next merge | Create a new branch from trunk |
|
||||
| `svn revert -R .` without a backup | Permanent loss of uncommitted work | Back up modified files first, or stash with a patch: `svn diff > my.patch` |
|
||||
| Committing secrets/credentials | Irrecoverable from SVN history without admin intervention | Use `.env` files excluded by `svnignore` |
|
||||
|
||||
### Saving work-in-progress without committing
|
||||
|
||||
SVN has no `git stash`. Use a patch file instead:
|
||||
|
||||
```bash
|
||||
svn diff > /tmp/my-wip.patch # save all local changes
|
||||
svn revert -R . # clean the WC
|
||||
# ... do the other work, commit it ...
|
||||
patch -p0 < /tmp/my-wip.patch # reapply your WIP
|
||||
```
|
||||
|
||||
### Setting up global ignores (one-time, per machine)
|
||||
|
||||
Add to `~/.subversion/config` under `[miscellany]`:
|
||||
|
||||
```ini
|
||||
global-ignores = *.o *.so *.log node_modules .env *.swp .DS_Store .specstory
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Last updated: April 2026 — AgMission / AgNav*
|
||||
Binary file not shown.
@ -1,48 +0,0 @@
|
||||
I.a. Upgrade/Install Erlang for Ubuntu version <= 18.04 LTS:
|
||||
# For Migrating from old Bintray repo's versions
|
||||
https://blog.rabbitmq.com/posts/2021/03/migrate-off-of-bintray/
|
||||
|
||||
# RabbitMq Team's PPA LaunchPad Erlang. Ref: https://launchpad.net/~rabbitmq/+archive/ubuntu/rabbitmq-erlang
|
||||
sudo add-apt-repository ppa:rabbitmq/rabbitmq-erlang
|
||||
sudo apt update
|
||||
## Uninstall/Upgrade erlang. This will also remove depended installed RabbitMq old version 3.7/8.*
|
||||
sudo apt-get purge erlang*
|
||||
|
||||
## Install erlang
|
||||
sudo apt install erlang
|
||||
# Or
|
||||
sudo apt-get install -y erlang-base \
|
||||
erlang-asn1 erlang-crypto erlang-eldap erlang-ftp erlang-inets \
|
||||
erlang-mnesia erlang-os-mon erlang-parsetools erlang-public-key \
|
||||
erlang-runtime-tools erlang-snmp erlang-ssl \
|
||||
erlang-syntax-tools erlang-tftp erlang-tools erlang-xmerl
|
||||
|
||||
# Install Rabbitmq online repository and make it ready to install the rabbitmq-server package later
|
||||
https://packagecloud.io/rabbitmq/rabbitmq-server/install#bash-deb
|
||||
|
||||
I.b For ubuntu version >= 18. Ref: https://www.rabbitmq.com/install-debian.html#installation-methods
|
||||
|
||||
II. Upgrade/Install RabbitMq to current version
|
||||
## Add a new admin user
|
||||
sudo rabbitmqctl add_user agm Ag@Rabbit2019
|
||||
sudo rabbitmqctl set_user_tags agm administrator
|
||||
sudo rabbitmqctl set_permissions -p / agm ".*" ".*" ".*"
|
||||
|
||||
sudo rabbitmq-server start -detached
|
||||
sudo rabbitmqctl stop
|
||||
# Web Management via port 15672
|
||||
## Enable web management plugins
|
||||
sudo rabbitmq-plugins enable rabbitmq_management
|
||||
|
||||
# Run Web management app
|
||||
http://localhost:15672
|
||||
user: agm
|
||||
pwd: Ag@Rabbit2019
|
||||
|
||||
### Important notes on mis-match depencies errors. If it happened, we need to pin the erlang packages version to a right version not always the latest.
|
||||
For example, for RabbitMq version 3.10.2, erlang must be < 25.* in Ubuntu 18 (bionic)
|
||||
# /etc/apt/preferences.d/erlang
|
||||
Package: erlang* esl-erlang
|
||||
Pin: version 1:24.3*
|
||||
Pin-Priority: 501
|
||||
|
||||
@ -1,210 +0,0 @@
|
||||
# Upstream server for handling API call
|
||||
upstream agmission_server {
|
||||
server 127.0.0.1:7000;
|
||||
}
|
||||
|
||||
upstream track_server {
|
||||
server 127.0.0.1:6100;
|
||||
}
|
||||
|
||||
upstream track_server_secure {
|
||||
server 127.0.0.1:6101;
|
||||
}
|
||||
|
||||
# Expires map: defines the mapping between the file type and how long that kind of file should be cached
|
||||
map $sent_http_content_type $expires {
|
||||
default off;
|
||||
text/html epoch; #means no cache, as it is not a static page
|
||||
text/css modified;
|
||||
application/javascript modified;
|
||||
application/woff2 max;
|
||||
}
|
||||
# ~image/ 30d; #it is only the logo, so maybe I could change it once a month now
|
||||
|
||||
# Default server configuration
|
||||
#
|
||||
server {
|
||||
# SSL config
|
||||
listen *:443 ssl http2 default_server;
|
||||
listen [::]:443 ssl http2;
|
||||
|
||||
ssl on;
|
||||
ssl_certificate /etc/nginx/ssl/agm-bundle.crt;
|
||||
ssl_certificate_key /etc/nginx/ssl/agm-server.key;
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
|
||||
|
||||
http2_max_field_size 64k;
|
||||
http2_max_header_size 512k;
|
||||
|
||||
server_name agmission.agnav.com;
|
||||
|
||||
root /media/ssd1/agmission/dist;
|
||||
|
||||
index index.html index.htm;
|
||||
|
||||
expires $expires;
|
||||
|
||||
location / {
|
||||
# First attempt to serve request as file, then
|
||||
# as directory, then fall back to index.html.
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /reports/ {
|
||||
# Simple requests
|
||||
if ($request_method ~* "(GET|POST)") {
|
||||
add_header "Access-Control-Allow-Origin" *;
|
||||
}
|
||||
|
||||
# Preflighted requests
|
||||
if ($request_method = OPTIONS ) {
|
||||
add_header "Access-Control-Allow-Origin" *;
|
||||
add_header "Access-Control-Allow-Methods" "GET, POST, OPTIONS, HEAD";
|
||||
add_header "Access-Control-Allow-Headers" "Authorization, Origin, X-Requested-With, Content-Type, Accept";
|
||||
return 200;
|
||||
}
|
||||
alias /media/ssd1/agmission/reports/;
|
||||
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
|
||||
location /public/ {
|
||||
alias /media/ssd1/agmission/public/;
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
location /leaflet/ {
|
||||
alias /media/ssd1/agmission/node_modules/leaflet/dist/;
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
location /esri-leaflet/ {
|
||||
alias /media/ssd1/agmission/node_modules/esri-leaflet/dist/;
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_pass http://agmission_server/api/;
|
||||
}
|
||||
|
||||
location = /track/stream {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection '';
|
||||
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
chunked_transfer_encoding off;
|
||||
proxy_read_timeout 12h;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header content-type "application/json";
|
||||
|
||||
proxy_pass https://track_server_secure/track/stream;
|
||||
# access_log /home/trung/temp/logs.txt;
|
||||
}
|
||||
|
||||
|
||||
location /track/ {
|
||||
#proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_pass http://track_server/track/;
|
||||
}
|
||||
|
||||
|
||||
location /forecast/ {
|
||||
proxy_http_version 1.1; // Must be explicit ver 1.1, Darksky upstream server
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_pass https://api.darksky.net/forecast/;
|
||||
|
||||
#proxy_redirect off;
|
||||
}
|
||||
|
||||
|
||||
location /gmapapi/ {
|
||||
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_pass https://maps.googleapis.com/maps/api/;
|
||||
|
||||
#proxy_redirect off;
|
||||
}
|
||||
|
||||
location /report/ {
|
||||
return 301 http://$host$request_uri;
|
||||
}
|
||||
|
||||
# deny access to .htaccess files, if Apache's document root
|
||||
# concurs with nginx's one
|
||||
#
|
||||
#location ~ /\.ht {
|
||||
# deny all;
|
||||
#}
|
||||
}
|
||||
|
||||
server {
|
||||
listen *:80 default_server;
|
||||
|
||||
# Migrate legacy TrackerNav API to current with these route proxies
|
||||
location = /api/v1/tracks {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_pass http://track_server/track/tracks;
|
||||
}
|
||||
|
||||
location = /api/v1/auth/login {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_pass http://agmission_server/api/users/login;
|
||||
}
|
||||
|
||||
|
||||
location / { # the default location redirects to https
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
location /report/ {
|
||||
# Simple requests
|
||||
if ($request_method ~* "(GET|POST)") {
|
||||
add_header "Access-Control-Allow-Origin" *;
|
||||
}
|
||||
|
||||
# Preflighted requests
|
||||
if ($request_method = OPTIONS ) {
|
||||
add_header "Access-Control-Allow-Origin" *;
|
||||
add_header "Access-Control-Allow-Methods" "GET, POST, OPTIONS, HEAD";
|
||||
add_header "Access-Control-Allow-Headers" "Authorization, Origin, X-Requested-With, Content-Type, Accept";
|
||||
return 200;
|
||||
}
|
||||
|
||||
root /media/ssd1/agmission/.tmp;
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
# MongoDB server performance tuning notes
|
||||
## Ref:
|
||||
https://dzone.com/articles/tuning-linux-for-mongodb
|
||||
https://docs.mongodb.com/manual/tutorial/transparent-huge-pages/
|
||||
https://docs.mongodb.com/manual/administration/production-notes/#std-label-readahead
|
||||
https://www.mongodb.com/blog/post/performance-best-practices-transactions-and-read--write-concerns
|
||||
https://www.percona.com/blog/2018/09/27/automating-mongodb-log-rotation/
|
||||
|
||||
# Add blockdev command to set readahead in /etc/rc.local
|
||||
# Set readahead for mongodb storage device
|
||||
blockdev --setra 32 /dev/<mounted device>
|
||||
|
||||
Future Further Improvements:
|
||||
- It is best to use RAID 1+0 with 4 SSDs for MongoDB server storage
|
||||
- Further tuning with readahead from 16 to 32 sectors of 512 bytes (16 KBs)
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
# /etc/logrotate.d/mongod.conf
|
||||
|
||||
/var/log/mongodb/mongod.log {
|
||||
daily
|
||||
size 2M
|
||||
rotate 14
|
||||
missingok
|
||||
compress
|
||||
delaycompress
|
||||
notifempty
|
||||
create 640 mongodb mongodb
|
||||
sharedscripts
|
||||
postrotate
|
||||
/bin/kill -SIGUSR1 `pidof mongod` >/dev/null 2>&1
|
||||
endscript
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
# mongod.conf
|
||||
|
||||
# for documentation of all options, see:
|
||||
# http://docs.mongodb.org/manual/reference/configuration-options/
|
||||
|
||||
# Where and how to store data.
|
||||
storage:
|
||||
dbPath: /media/ssd1/mdbdata
|
||||
journal:
|
||||
enabled: true
|
||||
# engine:
|
||||
# mmapv1:
|
||||
wiredTiger:
|
||||
engineConfig:
|
||||
cacheSizeGB: 3.0
|
||||
|
||||
# where to write logging data.
|
||||
systemLog:
|
||||
destination: file
|
||||
logAppend: true
|
||||
path: /var/log/mongodb/mongod.log
|
||||
logRotate: reopen
|
||||
|
||||
# network interfaces
|
||||
net:
|
||||
port: 27017
|
||||
bindIp: 0.0.0.0
|
||||
# bindIp: 127.0.0.1,208.73.207.98
|
||||
|
||||
|
||||
# how the process runs
|
||||
processManagement:
|
||||
timeZoneInfo: /usr/share/zoneinfo
|
||||
|
||||
security:
|
||||
authorization: enabled
|
||||
|
||||
#operationProfiling:
|
||||
|
||||
replication:
|
||||
replSetName: rs0
|
||||
|
||||
#sharding:
|
||||
|
||||
## Enterprise-Only Options:
|
||||
|
||||
#auditLog:
|
||||
|
||||
#snmp:
|
||||
@ -1,55 +0,0 @@
|
||||
# mongod.conf
|
||||
|
||||
# for documentation of all options, see:
|
||||
# http://docs.mongodb.org/manual/reference/configuration-options/
|
||||
|
||||
# Where and how to store data.
|
||||
storage:
|
||||
# dbPath:/var/lib/mongodb
|
||||
dbPath: /media/data/mongodb
|
||||
journal:
|
||||
enabled: true
|
||||
|
||||
# engine:
|
||||
# mmapv1:
|
||||
# wiredTiger:
|
||||
|
||||
# where to write logging data.
|
||||
systemLog:
|
||||
destination: file
|
||||
logAppend: true
|
||||
path: "/var/log/mongodb/mongod.log"
|
||||
logRotate: reopen
|
||||
|
||||
# network interfaces
|
||||
net:
|
||||
port: 27017
|
||||
bindIp: 0.0.0.0
|
||||
tls:
|
||||
mode: preferTLS
|
||||
certificateKeyFile: /etc/mongodb/ssl/localhost.pem
|
||||
CAFile: /etc/mongodb/ssl/rootCA.crt
|
||||
clusterFile: /etc/mongodb/ssl/localhost.pem
|
||||
|
||||
# how the process runs
|
||||
processManagement:
|
||||
timeZoneInfo: /usr/share/zoneinfo
|
||||
|
||||
security:
|
||||
# authorization: enabled
|
||||
# keyFile or x.509 cert is mandantary from mongo version 4.4
|
||||
# keyFile: /media/data/mongodb.key
|
||||
clusterAuthMode: x509
|
||||
|
||||
#operationProfiling:
|
||||
|
||||
replication:
|
||||
replSetName: rs0
|
||||
|
||||
#sharding:
|
||||
|
||||
## Enterprise-Only Options:
|
||||
|
||||
#auditLog:
|
||||
|
||||
#snmp:
|
||||
@ -1,9 +0,0 @@
|
||||
# /etc/security/limits.d/mongod.conf
|
||||
|
||||
mongodb soft nproc 64000
|
||||
|
||||
mongodb hard nproc 64000
|
||||
|
||||
mongodb soft nofile 64000
|
||||
|
||||
mongodb hard nofile 64000
|
||||
@ -1,72 +0,0 @@
|
||||
#
|
||||
# /etc/sysctl.conf - Configuration file for setting system variables
|
||||
# See /etc/sysctl.d/ for additional system variables.
|
||||
# See sysctl.conf (5) for information.
|
||||
#
|
||||
|
||||
#kernel.domainname = example.com
|
||||
|
||||
# Uncomment the following to stop low-level messages on console
|
||||
#kernel.printk = 3 4 1 3
|
||||
|
||||
##############################################################3
|
||||
# Functions previously found in netbase
|
||||
#
|
||||
|
||||
# Uncomment the next two lines to enable Spoof protection (reverse-path filter)
|
||||
# Turn on Source Address Verification in all interfaces to
|
||||
# prevent some spoofing attacks
|
||||
#net.ipv4.conf.default.rp_filter=1
|
||||
#net.ipv4.conf.all.rp_filter=1
|
||||
|
||||
# Uncomment the next line to enable TCP/IP SYN cookies
|
||||
# See http://lwn.net/Articles/277146/
|
||||
# Note: This may impact IPv6 TCP sessions too
|
||||
#net.ipv4.tcp_syncookies=1
|
||||
|
||||
# Uncomment the next line to enable packet forwarding for IPv4
|
||||
#net.ipv4.ip_forward=1
|
||||
|
||||
# Uncomment the next line to enable packet forwarding for IPv6
|
||||
# Enabling this option disables Stateless Address Autoconfiguration
|
||||
# based on Router Advertisements for this host
|
||||
#net.ipv6.conf.all.forwarding=1
|
||||
|
||||
|
||||
###################################################################
|
||||
# Additional settings - these settings can improve the network
|
||||
# security of the host and prevent against some network attacks
|
||||
# including spoofing attacks and man in the middle attacks through
|
||||
# redirection. Some network environments, however, require that these
|
||||
# settings are disabled so review and enable them as needed.
|
||||
#
|
||||
# Do not accept ICMP redirects (prevent MITM attacks)
|
||||
#net.ipv4.conf.all.accept_redirects = 0
|
||||
#net.ipv6.conf.all.accept_redirects = 0
|
||||
# _or_
|
||||
# Accept ICMP redirects only for gateways listed in our default
|
||||
# gateway list (enabled by default)
|
||||
# net.ipv4.conf.all.secure_redirects = 1
|
||||
#
|
||||
# Do not send ICMP redirects (we are not a router)
|
||||
#net.ipv4.conf.all.send_redirects = 0
|
||||
#
|
||||
# Do not accept IP source route packets (we are not a router)
|
||||
#net.ipv4.conf.all.accept_source_route = 0
|
||||
#net.ipv6.conf.all.accept_source_route = 0
|
||||
#
|
||||
# Log Martian Packets
|
||||
#net.ipv4.conf.all.log_martians = 1
|
||||
#
|
||||
fs.inotify.max_user_watches=524288
|
||||
|
||||
vm.dirty_ratio=15
|
||||
vm.dirty_background_ratio=5
|
||||
vm.swappiness=1
|
||||
|
||||
net.core.somaxconn=4096
|
||||
net.ipv4.tcp_fin_timeout=30
|
||||
net.ipv4.tcp_keepalive_intvl=30
|
||||
net.ipv4.tcp_keepalive_time=120
|
||||
net.ipv4.tcp_max_syn_backlog=4096
|
||||
|
||||
@ -1,74 +0,0 @@
|
||||
user www-data;
|
||||
worker_processes auto;
|
||||
pid /run/nginx.pid;
|
||||
include /etc/nginx/modules-enabled/*.conf;
|
||||
|
||||
events {
|
||||
worker_connections 768;
|
||||
# multi_accept on;
|
||||
}
|
||||
|
||||
http {
|
||||
|
||||
##
|
||||
# Basic Settings
|
||||
##
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
# server_tokens off;
|
||||
|
||||
# server_names_hash_bucket_size 64;
|
||||
# server_name_in_redirect off;
|
||||
|
||||
# set client body max size
|
||||
client_max_body_size 150M;
|
||||
|
||||
# Custome timeout to avoid error “504: Gateway Timeout” in the upstream API
|
||||
proxy_read_timeout 180;
|
||||
proxy_connect_timeout 120;
|
||||
proxy_send_timeout 120;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
##
|
||||
# SSL Settings
|
||||
##
|
||||
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
##
|
||||
# Logging Settings
|
||||
##
|
||||
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
##
|
||||
# Gzip Settings
|
||||
##
|
||||
|
||||
gzip on;
|
||||
gzip_disable "msie6";
|
||||
|
||||
# gzip_vary on;
|
||||
# gzip_proxied any;
|
||||
# gzip_comp_level 6;
|
||||
# gzip_buffers 16 8k;
|
||||
# gzip_http_version 1.1;
|
||||
# gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
|
||||
##
|
||||
# Virtual Host Configs
|
||||
##
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
include /etc/nginx/sites-enabled/*;
|
||||
}
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
# NgInx service config file. In Ubuntu 22 LTS path: /lib/systemd/system/nginx.service
|
||||
# Stop dance for nginx
|
||||
# =======================
|
||||
#
|
||||
# ExecStop sends SIGSTOP (graceful stop) to the nginx process.
|
||||
# If, after 5s (--retry QUIT/5) nginx is still running, systemd takes control
|
||||
# and sends SIGTERM (fast shutdown) to the main process.
|
||||
# After another 5s (TimeoutStopSec=5), and if nginx is alive, systemd sends
|
||||
# SIGKILL to all the remaining processes in the process group (KillMode=mixed).
|
||||
#
|
||||
# nginx signals reference doc:
|
||||
# http://nginx.org/en/docs/control.html
|
||||
#
|
||||
[Unit]
|
||||
Description=A high performance web server and a reverse proxy server
|
||||
Documentation=man:nginx(8)
|
||||
After=network.target nss-lookup.target
|
||||
#Ref: https://systemd.io/NETWORK_ONLINE/
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=forking
|
||||
PIDFile=/run/nginx.pid
|
||||
ExecStartPre=/usr/sbin/nginx -t -q -g 'daemon on; master_process on;'
|
||||
ExecStart=/usr/sbin/nginx -g 'daemon on; master_process on;'
|
||||
ExecReload=/usr/sbin/nginx -g 'daemon on; master_process on;' -s reload
|
||||
ExecStop=-/sbin/start-stop-daemon --quiet --stop --retry QUIT/5 --pidfile /run/nginx.pid
|
||||
TimeoutStopSec=5
|
||||
KillMode=mixed
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
|
||||
@ -1,81 +0,0 @@
|
||||
I - Making Mongo SSL certs for Mongo DB Server replica set
|
||||
# Check openssl version
|
||||
openssl version
|
||||
# openssl options
|
||||
openssl genrsa: Generates a private key
|
||||
openssl req: Generates a CSR
|
||||
openssl x509: Generates the certificate
|
||||
|
||||
1. Create Private Key for the root CA issuer
|
||||
openssl genrsa -passout file:./rootCA/pphrase -out ./rootCA/rootCA.key -aes256
|
||||
|
||||
2. Create Root CA certificate
|
||||
openssl req -x509 -new -key ./rootCA/rootCA.key -days 7300 -config ./root-ssl-config.cnf -out ./rootCA/rootCA.crt
|
||||
# View the certificate
|
||||
openssl x509 -noout -text -in ./rootCA/rootCA.crt
|
||||
|
||||
3. Create CSR for each of the member servers/hosts
|
||||
SUBJECT="/C=CA/ST=ON/L=Barrie/O=AG-NAV Inc./OU=SD/CN=localhost/emailAddress=software@agnav.com"
|
||||
openssl req -new -nodes -newkey rsa:2048 -subj "/C=CA/ST=ON/L=Barrie/O=AG-NAV Inc./OU=SD/CN=localhost/emailAddress=software@agnav.com" -keyout server1.key -out server1.csr
|
||||
|
||||
or using the shell script:
|
||||
./makeCSR <hostname>
|
||||
|
||||
4. Sign the CSR then create certificate for the member
|
||||
openssl x509 -req -days 7300 -in server1.csr -CA ./rootCA/rootCA.crt -CAkey ./rootCA/rootCA.key -CAcreateserial -out ./server1.crt -sha256 -extfile v3-ext.cnf
|
||||
|
||||
5. Create a privacy enhanced mail (PEM) for mongod
|
||||
cat server1.key server1.crt > server1.pem
|
||||
|
||||
or using the shell script (for step 4 and 5):
|
||||
./makeCert <hostname>
|
||||
|
||||
6. Deploy (root)
|
||||
Move <hostname>.crt/pem/csr file to /etc/ssl/certs/
|
||||
Move <hostname>.key (private key) file to /etc/ssl/private
|
||||
Change permission for all to readonly 440
|
||||
|
||||
# (Optional) Create appcerts usergroup, then add users: root,www-data,mongodb,rabbitmq to the group
|
||||
|
||||
|
||||
Reference:
|
||||
https://www.bustedware.com/blog/mongodb-ssl-tls-x509-authentication#create-certificate-authority
|
||||
https://www.mydbops.com/blog/securing-mongodb-cluster-with-tls-ssl
|
||||
https://www.ibm.com/docs/en/hpvs/1.2.x?topic=SSHPMH_1.2.x/topics/create_ca_signed_certificates.htm
|
||||
https://www.mydbops.com/blog/securing-mongodb-cluster-with-tls-ssl#
|
||||
https://www.filecloud.com/supportdocs/fcdoc/latest/server/filecloud-administrator-guide/filecloud-site-setup/filecloud-high-availability/configure-mongodb-cluster-to-use-tls-ssl-with-cluster-authentication-and-mongodb-authentication-on-linux
|
||||
|
||||
|
||||
II - Replace/Renew/Rotate SSL x509 certs for a replica set
|
||||
1. Make CSRs
|
||||
./makeCSR.sh agndb0.agnav.com
|
||||
./makeCSR.sh agndb1.agnav.com
|
||||
./makeCSR.sh agndb2.agnav.com
|
||||
|
||||
2. Make Certs
|
||||
./makeCert.sh agndb0.agnav.com
|
||||
./makeCert.sh agndb1.agnav.com
|
||||
./makeCert.sh agndb2.agnav.com
|
||||
|
||||
3. Copy them to each nodes to deploy
|
||||
scp ./rootCA/rootCA.crt agnav@kanboard.agnav.com:~/agnav_rootCA.crt
|
||||
scp agndb2* agnav@kanboard.agnav.com:~/
|
||||
scp -P 22222 agndb1* agm@agmission-1.agnav.com:~/
|
||||
scp -P 22889 agndb0* agmission@agmission.agnav.com:~/
|
||||
|
||||
## Check after copying
|
||||
ssh agnav@kanboard.agnav.com 'ls ~/'
|
||||
ssh -p 22222 agm@agmission-1.agnav.com 'ls ~/'
|
||||
ssh -p 22889 agmission@agmission.agnav.com 'ls ~/'
|
||||
|
||||
3.2 Copy them to the deploy storage location
|
||||
|
||||
4. Restart each of the member from secondaries to the primary one
|
||||
|
||||
5. Verify they all work.
|
||||
mongo -u admin -p 'Minad!2019' --authenticationDatabase 'admin'
|
||||
rs.status()
|
||||
tail -n 500 /var/log/mongodb/mongod.log
|
||||
|
||||
Referece:
|
||||
https://www.mongodb.com/docs/manual/tutorial/rotate-x509-membership-certificates/
|
||||
@ -1,27 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEqjCCA5KgAwIBAgIUckN8Se/RK/askfiTUwBPzwNzeg8wDQYJKoZIhvcNAQEL
|
||||
BQAwgYUxCzAJBgNVBAYTAkNBMQswCQYDVQQIDAJPTjEPMA0GA1UEBwwGQmFycmll
|
||||
MRQwEgYDVQQKDAtBRy1OQVYgSW5jLjELMAkGA1UECwwCU0QxEjAQBgNVBAMMCWFn
|
||||
bmF2LmNvbTEhMB8GCSqGSIb3DQEJARYSc29mdHdhcmVAYWduYXYuY29tMB4XDTI1
|
||||
MDExNDIwMTgxM1oXDTQ1MDEwOTIwMTgxM1owgYoxCzAJBgNVBAYTAkNBMQswCQYD
|
||||
VQQIDAJPTjEPMA0GA1UEBwwGQmFycmllMRQwEgYDVQQKDAtBRy1OQVYgSW5jLjEW
|
||||
MBQGA1UECwwNTU9OR09fQ0xJRU5UUzEMMAoGA1UEAwwDYWdtMSEwHwYJKoZIhvcN
|
||||
AQkBFhJzb2Z0d2FyZUBhZ25hdi5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
|
||||
ggEKAoIBAQDiFF/zeTPt6N1/VafvOXe/QvdagaCYGCKBq4YJIHHT552bBXAoYPLo
|
||||
bXWSJOZt4y3rYIzG3YcCuLZDAzUWZIPczl0PkXTwkPFBtiQQHRIQigTcNZXzPpuJ
|
||||
JXrSAHo9WGlkn/22TxYSspA0fV00AhLXvAk6mXtz+6klVIcJ5ujACgq8JDpmfklB
|
||||
5c89TvKB0Z9/qoAWTm+9lVvR7+VQ4i0BFooKCOKpEaLse3lb6hHhSxX4da0ICb7R
|
||||
6G7Xchne30SDitvRgWhiCuPc9WQDVXVsx3UNGi5ppNb79w/iEwMJ4/qxk5Ob/pjb
|
||||
xtibvVi2OpFHYiV2FJiOSzBeDD3y3zbjAgMBAAGjggEJMIIBBTCBrwYDVR0jBIGn
|
||||
MIGkoYGLpIGIMIGFMQswCQYDVQQGEwJDQTELMAkGA1UECAwCT04xDzANBgNVBAcM
|
||||
BkJhcnJpZTEUMBIGA1UECgwLQUctTkFWIEluYy4xCzAJBgNVBAsMAlNEMRIwEAYD
|
||||
VQQDDAlhZ25hdi5jb20xITAfBgkqhkiG9w0BCQEWEnNvZnR3YXJlQGFnbmF2LmNv
|
||||
bYIUUhbg9g1ttEbMvdhTD6/JouV4tcAwCQYDVR0TBAIwADALBgNVHQ8EBAMCBPAw
|
||||
GgYDVR0RBBMwEYIJbG9jYWxob3N0hwR/AAABMB0GA1UdDgQWBBQKirMifYvhfsET
|
||||
Ex6v2AsXC0yjfTANBgkqhkiG9w0BAQsFAAOCAQEAFCg+UqerRIyt5FWP7ZhIp2xi
|
||||
5E1LOro127AlMDk7oT/FuALpQCzP4qzzePTp3qamJxuteSph3IGAW2ltg8h9ocM+
|
||||
NNK4fVKtxg5XpYHOuxmRwr0/pmWXDogs8C/+sSbx2QES6BlFEZRih4mY0ND+3b0j
|
||||
lmc+wP4SNlUNDJD7Fhgwz9IiUlo7ok4Mr0TYRn26Yql4zazKFV4ShGdPCBUpPQ39
|
||||
Phl3pLGzyWw8DUcoeG/D7cwyFXyscNK79dcQt11gqJMRdO4sTymAPzlfTlzfcW8x
|
||||
xjizOmFWkI4DYGJgmyq5x4L5bUVeq4/WMWsHpYDFcUd3b8safG7RgPRSg2OgXQ==
|
||||
-----END CERTIFICATE-----
|
||||
@ -1,18 +0,0 @@
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIC0DCCAbgCAQAwgYoxCzAJBgNVBAYTAkNBMQswCQYDVQQIDAJPTjEPMA0GA1UE
|
||||
BwwGQmFycmllMRQwEgYDVQQKDAtBRy1OQVYgSW5jLjEWMBQGA1UECwwNTU9OR09f
|
||||
Q0xJRU5UUzEMMAoGA1UEAwwDYWdtMSEwHwYJKoZIhvcNAQkBFhJzb2Z0d2FyZUBh
|
||||
Z25hdi5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDiFF/zeTPt
|
||||
6N1/VafvOXe/QvdagaCYGCKBq4YJIHHT552bBXAoYPLobXWSJOZt4y3rYIzG3YcC
|
||||
uLZDAzUWZIPczl0PkXTwkPFBtiQQHRIQigTcNZXzPpuJJXrSAHo9WGlkn/22TxYS
|
||||
spA0fV00AhLXvAk6mXtz+6klVIcJ5ujACgq8JDpmfklB5c89TvKB0Z9/qoAWTm+9
|
||||
lVvR7+VQ4i0BFooKCOKpEaLse3lb6hHhSxX4da0ICb7R6G7Xchne30SDitvRgWhi
|
||||
CuPc9WQDVXVsx3UNGi5ppNb79w/iEwMJ4/qxk5Ob/pjbxtibvVi2OpFHYiV2FJiO
|
||||
SzBeDD3y3zbjAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEAdkaqqYUV+6RZ8oVq
|
||||
XrmE8JWlu1IRRnqXiMRQ8TW1/zjIjaGfHkflDYP0rnOYB7ifnv31sRlLyizTD4/6
|
||||
MIGlnmF28aK2CVf0TNzNeQosjs3FWirw0q8PLdsCFJetlM6LOwA+0RmN1mq5y/Vk
|
||||
r1ntpoK3LOFOacaWcDC92O0sNzD2gQI3abCpoELhiPDhIAhNJW3UdEy9H0UqGg6o
|
||||
Book8NHhxKZUAwstiOAVYdIg5RaDMJAPgD7czuVDuryP42efDSSqGtv9rBC/3N3u
|
||||
bhxKv7jkBGLqKwdSBF13NJZlIv5t+krQ6vVoquJlAebGDLxNCQTW8KW+UrlevlAF
|
||||
GvY58w==
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
@ -1,28 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDiFF/zeTPt6N1/
|
||||
VafvOXe/QvdagaCYGCKBq4YJIHHT552bBXAoYPLobXWSJOZt4y3rYIzG3YcCuLZD
|
||||
AzUWZIPczl0PkXTwkPFBtiQQHRIQigTcNZXzPpuJJXrSAHo9WGlkn/22TxYSspA0
|
||||
fV00AhLXvAk6mXtz+6klVIcJ5ujACgq8JDpmfklB5c89TvKB0Z9/qoAWTm+9lVvR
|
||||
7+VQ4i0BFooKCOKpEaLse3lb6hHhSxX4da0ICb7R6G7Xchne30SDitvRgWhiCuPc
|
||||
9WQDVXVsx3UNGi5ppNb79w/iEwMJ4/qxk5Ob/pjbxtibvVi2OpFHYiV2FJiOSzBe
|
||||
DD3y3zbjAgMBAAECggEAFY6zeYq982UdiHtOnfE2ZGlKdHm2KlqUIRY4cTKuopyh
|
||||
etCMGdA+HqtKpzVX4vQ4guBm0WhP6+gDZSTEpqPOA7uKbyYZEalYf+y8x+vltuha
|
||||
P0noHaouIufigWzPOyiXis32CjDEs41i/RupIVZDSAlOPfq4UAkYCg8N01lj1I/S
|
||||
niaEXOH90xdWSJlgTdn9aNmVLSyp/4t1jDvH4Y7MxHey6fNSd7nDNd4/jK97aH37
|
||||
Gg/4Qf8itIEBNU5wWjiq4oDwg2DJeBxlh8xz+mD6NpZvy/AF3ByTfGL6vXovY7QS
|
||||
3Q4asdmW+CfZJ+M9/AG8zghBRXN9WM9Pbcw1yTKrpQKBgQD+dAma5LlE5EvfyFk3
|
||||
8hal06xzSuiT3YFaY9CNIS4ytzCKlddv0TQxdOw0i94hDNjPb1WX9J+6hVGjAa66
|
||||
nOLzza+7iMPu9td2E4wYQ9Y8Vrxnh9Mr3zlnlJbX3U28+FyynFs5MbTifG5uUTec
|
||||
3CSyJkALZBSUJ7O97Hzbxy5MDwKBgQDjdC8jr6LrIKUYxe2rWptKnM0kmTxCGlbw
|
||||
eenyuLRhfzyhipB1K2JYOhl4Gqn3zwejekk7kHOwphjI2+3lAp/3y251LAuRSsn/
|
||||
mibIsVhgi/2BO5MCWBGkRzEr74M47FHVIqa7MO50oGDxmOE+AmuluAwqgm7ITIm2
|
||||
vgQ069dj7QKBgDQRxMFomq0JVql1kyRKqu3GMhzpsExJ4KWBlXS73HtOV2WUoiBk
|
||||
nByew5NBJ/R1b4yLSOWujl0Z2QnVV08iuaKQbayfoRCufIrSFzID97wjN6yr87+f
|
||||
j1yt4GxOAhFwdW+rZVN/43cRZXu3rPyxY+T8xNBP65IhybtMwIQs70FrAoGBAJUK
|
||||
zNjVfiwUeBqDl/lwpdF+be5Neu7V06JAQMyLu6cneNNhuMcOZqLpb0cEMdvwDVFS
|
||||
ECq3vRdDv3neo0QtNCVraDXfZrUODM8wc7mOfBrHoJXOM8aVbvn2rIHdsF7ce8Lt
|
||||
sdN3fMlvThcB1paLf35X26D/VxhpDtRwLtF+uOUBAoGAUN7Cq2YUiPXp44vuIuH3
|
||||
dFbRkV6FPU1ixYniIpA8UGA6GfDKjsFio29kjnsHCcW0UhnpRRph7vMLUXDh/8q/
|
||||
bY13rLHJVuSp4pEIw4rgF/EINhi8wHNoIG0yFDIcLCgi0IeJRJXhz4IK9UT+ueg3
|
||||
CqXJXBDslc04zKpOiOqjdSQ=
|
||||
-----END PRIVATE KEY-----
|
||||
@ -1,55 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDiFF/zeTPt6N1/
|
||||
VafvOXe/QvdagaCYGCKBq4YJIHHT552bBXAoYPLobXWSJOZt4y3rYIzG3YcCuLZD
|
||||
AzUWZIPczl0PkXTwkPFBtiQQHRIQigTcNZXzPpuJJXrSAHo9WGlkn/22TxYSspA0
|
||||
fV00AhLXvAk6mXtz+6klVIcJ5ujACgq8JDpmfklB5c89TvKB0Z9/qoAWTm+9lVvR
|
||||
7+VQ4i0BFooKCOKpEaLse3lb6hHhSxX4da0ICb7R6G7Xchne30SDitvRgWhiCuPc
|
||||
9WQDVXVsx3UNGi5ppNb79w/iEwMJ4/qxk5Ob/pjbxtibvVi2OpFHYiV2FJiOSzBe
|
||||
DD3y3zbjAgMBAAECggEAFY6zeYq982UdiHtOnfE2ZGlKdHm2KlqUIRY4cTKuopyh
|
||||
etCMGdA+HqtKpzVX4vQ4guBm0WhP6+gDZSTEpqPOA7uKbyYZEalYf+y8x+vltuha
|
||||
P0noHaouIufigWzPOyiXis32CjDEs41i/RupIVZDSAlOPfq4UAkYCg8N01lj1I/S
|
||||
niaEXOH90xdWSJlgTdn9aNmVLSyp/4t1jDvH4Y7MxHey6fNSd7nDNd4/jK97aH37
|
||||
Gg/4Qf8itIEBNU5wWjiq4oDwg2DJeBxlh8xz+mD6NpZvy/AF3ByTfGL6vXovY7QS
|
||||
3Q4asdmW+CfZJ+M9/AG8zghBRXN9WM9Pbcw1yTKrpQKBgQD+dAma5LlE5EvfyFk3
|
||||
8hal06xzSuiT3YFaY9CNIS4ytzCKlddv0TQxdOw0i94hDNjPb1WX9J+6hVGjAa66
|
||||
nOLzza+7iMPu9td2E4wYQ9Y8Vrxnh9Mr3zlnlJbX3U28+FyynFs5MbTifG5uUTec
|
||||
3CSyJkALZBSUJ7O97Hzbxy5MDwKBgQDjdC8jr6LrIKUYxe2rWptKnM0kmTxCGlbw
|
||||
eenyuLRhfzyhipB1K2JYOhl4Gqn3zwejekk7kHOwphjI2+3lAp/3y251LAuRSsn/
|
||||
mibIsVhgi/2BO5MCWBGkRzEr74M47FHVIqa7MO50oGDxmOE+AmuluAwqgm7ITIm2
|
||||
vgQ069dj7QKBgDQRxMFomq0JVql1kyRKqu3GMhzpsExJ4KWBlXS73HtOV2WUoiBk
|
||||
nByew5NBJ/R1b4yLSOWujl0Z2QnVV08iuaKQbayfoRCufIrSFzID97wjN6yr87+f
|
||||
j1yt4GxOAhFwdW+rZVN/43cRZXu3rPyxY+T8xNBP65IhybtMwIQs70FrAoGBAJUK
|
||||
zNjVfiwUeBqDl/lwpdF+be5Neu7V06JAQMyLu6cneNNhuMcOZqLpb0cEMdvwDVFS
|
||||
ECq3vRdDv3neo0QtNCVraDXfZrUODM8wc7mOfBrHoJXOM8aVbvn2rIHdsF7ce8Lt
|
||||
sdN3fMlvThcB1paLf35X26D/VxhpDtRwLtF+uOUBAoGAUN7Cq2YUiPXp44vuIuH3
|
||||
dFbRkV6FPU1ixYniIpA8UGA6GfDKjsFio29kjnsHCcW0UhnpRRph7vMLUXDh/8q/
|
||||
bY13rLHJVuSp4pEIw4rgF/EINhi8wHNoIG0yFDIcLCgi0IeJRJXhz4IK9UT+ueg3
|
||||
CqXJXBDslc04zKpOiOqjdSQ=
|
||||
-----END PRIVATE KEY-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEqjCCA5KgAwIBAgIUckN8Se/RK/askfiTUwBPzwNzeg8wDQYJKoZIhvcNAQEL
|
||||
BQAwgYUxCzAJBgNVBAYTAkNBMQswCQYDVQQIDAJPTjEPMA0GA1UEBwwGQmFycmll
|
||||
MRQwEgYDVQQKDAtBRy1OQVYgSW5jLjELMAkGA1UECwwCU0QxEjAQBgNVBAMMCWFn
|
||||
bmF2LmNvbTEhMB8GCSqGSIb3DQEJARYSc29mdHdhcmVAYWduYXYuY29tMB4XDTI1
|
||||
MDExNDIwMTgxM1oXDTQ1MDEwOTIwMTgxM1owgYoxCzAJBgNVBAYTAkNBMQswCQYD
|
||||
VQQIDAJPTjEPMA0GA1UEBwwGQmFycmllMRQwEgYDVQQKDAtBRy1OQVYgSW5jLjEW
|
||||
MBQGA1UECwwNTU9OR09fQ0xJRU5UUzEMMAoGA1UEAwwDYWdtMSEwHwYJKoZIhvcN
|
||||
AQkBFhJzb2Z0d2FyZUBhZ25hdi5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
|
||||
ggEKAoIBAQDiFF/zeTPt6N1/VafvOXe/QvdagaCYGCKBq4YJIHHT552bBXAoYPLo
|
||||
bXWSJOZt4y3rYIzG3YcCuLZDAzUWZIPczl0PkXTwkPFBtiQQHRIQigTcNZXzPpuJ
|
||||
JXrSAHo9WGlkn/22TxYSspA0fV00AhLXvAk6mXtz+6klVIcJ5ujACgq8JDpmfklB
|
||||
5c89TvKB0Z9/qoAWTm+9lVvR7+VQ4i0BFooKCOKpEaLse3lb6hHhSxX4da0ICb7R
|
||||
6G7Xchne30SDitvRgWhiCuPc9WQDVXVsx3UNGi5ppNb79w/iEwMJ4/qxk5Ob/pjb
|
||||
xtibvVi2OpFHYiV2FJiOSzBeDD3y3zbjAgMBAAGjggEJMIIBBTCBrwYDVR0jBIGn
|
||||
MIGkoYGLpIGIMIGFMQswCQYDVQQGEwJDQTELMAkGA1UECAwCT04xDzANBgNVBAcM
|
||||
BkJhcnJpZTEUMBIGA1UECgwLQUctTkFWIEluYy4xCzAJBgNVBAsMAlNEMRIwEAYD
|
||||
VQQDDAlhZ25hdi5jb20xITAfBgkqhkiG9w0BCQEWEnNvZnR3YXJlQGFnbmF2LmNv
|
||||
bYIUUhbg9g1ttEbMvdhTD6/JouV4tcAwCQYDVR0TBAIwADALBgNVHQ8EBAMCBPAw
|
||||
GgYDVR0RBBMwEYIJbG9jYWxob3N0hwR/AAABMB0GA1UdDgQWBBQKirMifYvhfsET
|
||||
Ex6v2AsXC0yjfTANBgkqhkiG9w0BAQsFAAOCAQEAFCg+UqerRIyt5FWP7ZhIp2xi
|
||||
5E1LOro127AlMDk7oT/FuALpQCzP4qzzePTp3qamJxuteSph3IGAW2ltg8h9ocM+
|
||||
NNK4fVKtxg5XpYHOuxmRwr0/pmWXDogs8C/+sSbx2QES6BlFEZRih4mY0ND+3b0j
|
||||
lmc+wP4SNlUNDJD7Fhgwz9IiUlo7ok4Mr0TYRn26Yql4zazKFV4ShGdPCBUpPQ39
|
||||
Phl3pLGzyWw8DUcoeG/D7cwyFXyscNK79dcQt11gqJMRdO4sTymAPzlfTlzfcW8x
|
||||
xjizOmFWkI4DYGJgmyq5x4L5bUVeq4/WMWsHpYDFcUd3b8safG7RgPRSg2OgXQ==
|
||||
-----END CERTIFICATE-----
|
||||
@ -1,22 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDkzCCAnsCFFIW4PYNbbRGzL3YUw+vyaLleLXAMA0GCSqGSIb3DQEBCwUAMIGF
|
||||
MQswCQYDVQQGEwJDQTELMAkGA1UECAwCT04xDzANBgNVBAcMBkJhcnJpZTEUMBIG
|
||||
A1UECgwLQUctTkFWIEluYy4xCzAJBgNVBAsMAlNEMRIwEAYDVQQDDAlhZ25hdi5j
|
||||
b20xITAfBgkqhkiG9w0BCQEWEnNvZnR3YXJlQGFnbmF2LmNvbTAeFw0yNTAxMTAx
|
||||
NzE3NDBaFw00NTAxMDUxNzE3NDBaMIGFMQswCQYDVQQGEwJDQTELMAkGA1UECAwC
|
||||
T04xDzANBgNVBAcMBkJhcnJpZTEUMBIGA1UECgwLQUctTkFWIEluYy4xCzAJBgNV
|
||||
BAsMAlNEMRIwEAYDVQQDDAlhZ25hdi5jb20xITAfBgkqhkiG9w0BCQEWEnNvZnR3
|
||||
YXJlQGFnbmF2LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPO6
|
||||
zGIpSaBKYliTuGBvMWxXzrCvUUlqYAB2vJeBJf8prbfL2rRM5ac1vXx3HOFQOXF5
|
||||
dwJz+XaN45xkfYLNPta6kGTn0RDaFUpoE70SIsEP7Q1D3FhXMAocXhfSy6sZeZB/
|
||||
XKJ0dCjuVDOxjle6ksQdiKrJFzVF1xWwWijGD66jzDIcQJTCJqxyMblt/YQaI+q8
|
||||
uL2/wjiNRYALtSpd77HOQ075/Gd2sN8ppDCkpcg1F6Sx1Z9WcQx9pB/pI8wuuvdg
|
||||
xJsO1xpLsvltFMBcFTfIZYJckNPL/wEFL6XSuFBZ6S8LLTeNL8vk2JfZXai3rsXb
|
||||
LyTrFvVOavtcltcI5UECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA374T4MtxTCU/
|
||||
9VR7+aRjXb5QnTTBtigCGjo5la8eIQZiFnk8FJJZEIYI4obphTleAKAfypDlQ+Wj
|
||||
JY/4bOWvnjZIHi7XOnGLx6n9fSv4pC2PzMHThwHvY+vPGrHtoYT1xtfCRzaAMdvS
|
||||
kXbknniLS+QarurADQA5MT4v+gGuK4dq+LvgPWOhmr3/VTmdfFJQCb0J/JD8CcEu
|
||||
MSABLSIwmcu0tDd7647SkTQKBM5vdQVL15eBCMwf/sIYKokqy+It9xRzdFsEYviz
|
||||
sK2CtuheeEhJBU98ACAS2gK5CZKNTrSFNk5uTlVfUKMdF6FMmQ/vSPX+xfRpkS3R
|
||||
AWeVHLZ4OQ==
|
||||
-----END CERTIFICATE-----
|
||||
@ -1,27 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEqDCCA5CgAwIBAgIUckN8Se/RK/askfiTUwBPzwNzehAwDQYJKoZIhvcNAQEL
|
||||
BQAwgYUxCzAJBgNVBAYTAkNBMQswCQYDVQQIDAJPTjEPMA0GA1UEBwwGQmFycmll
|
||||
MRQwEgYDVQQKDAtBRy1OQVYgSW5jLjELMAkGA1UECwwCU0QxEjAQBgNVBAMMCWFn
|
||||
bmF2LmNvbTEhMB8GCSqGSIb3DQEJARYSc29mdHdhcmVAYWduYXYuY29tMB4XDTI1
|
||||
MDEyNzIwMjk0M1oXDTQ1MDEyMjIwMjk0M1owgYwxCzAJBgNVBAYTAkNBMQswCQYD
|
||||
VQQIDAJPTjEPMA0GA1UEBwwGQmFycmllMRQwEgYDVQQKDAtBRy1OQVYgSW5jLjEL
|
||||
MAkGA1UECwwCU0QxGTAXBgNVBAMMEGFnbmRiMC5hZ25hdi5jb20xITAfBgkqhkiG
|
||||
9w0BCQEWEnNvZnR3YXJlQGFnbmF2LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEP
|
||||
ADCCAQoCggEBAJlmik4GVXoS5c+695H7EWe9p2z5WQIOOMFla3Lytt+vapuHVPJy
|
||||
rZ5hFW3lq9vW5BFzTJHpThT0Ff89O2FvfEP04RATIKqUkK8I/RnwyOAoDKB4PUdX
|
||||
qGsf+1qMvav3c5Ma3fRGWPBTe+rme+NeclaNXzqTUApCGc8mYHDugC3s0kZtHl+Z
|
||||
MKZ+xONdhRDCa18Ko0Kfs6obMPK8fqYdrRYd53uPXCAnNXoQAXNT67QL/uNeMJxC
|
||||
kT/tV58r4eH0n1qU9pXMR4UmMHPs8HG47Sdk67LUhY6sJASTXQB3fKdTBt72oLsS
|
||||
gUZ5BpSUQaXk0mtiuL5rBYEn7HhRr1JhYAsCAwEAAaOCAQUwggEBMIGvBgNVHSME
|
||||
gacwgaShgYukgYgwgYUxCzAJBgNVBAYTAkNBMQswCQYDVQQIDAJPTjEPMA0GA1UE
|
||||
BwwGQmFycmllMRQwEgYDVQQKDAtBRy1OQVYgSW5jLjELMAkGA1UECwwCU0QxEjAQ
|
||||
BgNVBAMMCWFnbmF2LmNvbTEhMB8GCSqGSIb3DQEJARYSc29mdHdhcmVAYWduYXYu
|
||||
Y29tghRSFuD2DW20Rsy92FMPr8mi5Xi1wDAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE
|
||||
8DAWBgNVHREEDzANggsqLmFnbmF2LmNvbTAdBgNVHQ4EFgQUTZxBYBMPCDnCf6zx
|
||||
eK9cPLAqYUgwDQYJKoZIhvcNAQELBQADggEBADeFFlZiKeAkKsqeCyBYa70iv8lB
|
||||
54bzIIjdvQUctDkfjkmZo5rwsGvtwD8peNFegKy7BiXzDZYJ0ZlAAEZ5bUazjnYS
|
||||
SHEqid8WVeCnsgp9TWi8uMX4BaVTJr2LvSsYF7SrlCTx+aRPLes2CP3FPXSFo81K
|
||||
zs9p9mPAHPbtByHb/CIkwyh2HX4Ap0SDYGAOVf9mC7sDWfDWTiIWSAl4YbAVily9
|
||||
E1JqwPMH8aHNBOTpXZPYto4TIrF4vVIeXP/EVrKVfbeZMNwNFfrRqWcAPMAXX3hE
|
||||
XTFzhFeepdXExckC6ulNmfLaitxLp9L0J8NqCzrLC2WIoau5CXqGR1Ux7FY=
|
||||
-----END CERTIFICATE-----
|
||||
@ -1,18 +0,0 @@
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIC0jCCAboCAQAwgYwxCzAJBgNVBAYTAkNBMQswCQYDVQQIDAJPTjEPMA0GA1UE
|
||||
BwwGQmFycmllMRQwEgYDVQQKDAtBRy1OQVYgSW5jLjELMAkGA1UECwwCU0QxGTAX
|
||||
BgNVBAMMEGFnbmRiMC5hZ25hdi5jb20xITAfBgkqhkiG9w0BCQEWEnNvZnR3YXJl
|
||||
QGFnbmF2LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJlmik4G
|
||||
VXoS5c+695H7EWe9p2z5WQIOOMFla3Lytt+vapuHVPJyrZ5hFW3lq9vW5BFzTJHp
|
||||
ThT0Ff89O2FvfEP04RATIKqUkK8I/RnwyOAoDKB4PUdXqGsf+1qMvav3c5Ma3fRG
|
||||
WPBTe+rme+NeclaNXzqTUApCGc8mYHDugC3s0kZtHl+ZMKZ+xONdhRDCa18Ko0Kf
|
||||
s6obMPK8fqYdrRYd53uPXCAnNXoQAXNT67QL/uNeMJxCkT/tV58r4eH0n1qU9pXM
|
||||
R4UmMHPs8HG47Sdk67LUhY6sJASTXQB3fKdTBt72oLsSgUZ5BpSUQaXk0mtiuL5r
|
||||
BYEn7HhRr1JhYAsCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQAMX9rqU5S4AxBR
|
||||
MiAEXzaJbI4rI8jf9GthZQT++EQRWN07CcRvw29xtOe22m5CSwkZw/1aBYGuxxIi
|
||||
+Cgc1YB6a3rFE2KuWciqj4+IAG56+oHDxW1/xkd+9f/HaN2NCuf2/AuSklBkadmB
|
||||
j6lYOUYcKJuj3y9CHneqAzMzB9SzUKt/Q8u5ywxwXXMxiAn2va3gUbylIzMnQ6tr
|
||||
aZtGmEKpyS/G1IFd0V49bxtlomZ9ygda/YbOnpHGdzqrwq4mnlqZuGmtAg5CGIzA
|
||||
scQb5qI0/e03u4NJeDdcpc20B2H+U+7B2jXXXj47Bk/iGm4RvYHS1exkYycUxSPu
|
||||
6kJMh+nH
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
@ -1,28 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCZZopOBlV6EuXP
|
||||
uveR+xFnvads+VkCDjjBZWty8rbfr2qbh1Tycq2eYRVt5avb1uQRc0yR6U4U9BX/
|
||||
PTthb3xD9OEQEyCqlJCvCP0Z8MjgKAygeD1HV6hrH/tajL2r93OTGt30RljwU3vq
|
||||
5nvjXnJWjV86k1AKQhnPJmBw7oAt7NJGbR5fmTCmfsTjXYUQwmtfCqNCn7OqGzDy
|
||||
vH6mHa0WHed7j1wgJzV6EAFzU+u0C/7jXjCcQpE/7VefK+Hh9J9alPaVzEeFJjBz
|
||||
7PBxuO0nZOuy1IWOrCQEk10Ad3ynUwbe9qC7EoFGeQaUlEGl5NJrYri+awWBJ+x4
|
||||
Ua9SYWALAgMBAAECggEAArg5YH7zpsyzkiMmJRdQUvufGCsz8EZ9rBp9Tq+0UH/r
|
||||
W6MisILm+bIn72eNTFvH3FiKdXwvT02XUqVskMj2T4m4ycOrcxeO/x0HyINz1EGL
|
||||
HV2awutWoY7GGPAto3rC6+TW8lZvvwcD4Rsn/CGrB+AAXbX9F8l3ORLczkCN0wKW
|
||||
TEuRWZ8gPfZ4Jp84ST0B//qcy6Se9Hb+03k8VC9WJWtoE5uT/Wn4M0yEfdVENdWk
|
||||
7tjr7WRuwWz+PS5ydu9reAOJV0cHJj34NdoFdwXf4Jf5+sngI4meRhhF+8T/ezHA
|
||||
/vY3+KaY7QCfjPcncTc6BM+GV8V2sVzPbz7BuGjxdQKBgQDVlOpWaZIC2sqAtRmV
|
||||
krnHeJTKwmLPlPoZED4q88Bcb4a87F7fHnhQOVajBEaSqBWcbBRD+LhQWdr+3CmX
|
||||
teJqJFLg6oOPvOSpJY7/+QU2LltefoYPTlHjwIEm2lV/10IyqceLnuWDi4e34m2S
|
||||
fljKcemGBJgQWJ2qruFJQWAY1QKBgQC33dk6ZvOqcC1VgB+71Z2j4ZD5S6+SkGRi
|
||||
OwIJ+F7fXgvw+/nH/HhnscVVuDuKjewf3Mg+k3cCHTs0FSaLs6o2N9JBd6VWlG4z
|
||||
HSN13u0FrmZtEOjXGuJS1jRu8LUGFx0xB1/VfGDRT9/B7irprjJfLCOy5FiYTlkJ
|
||||
QqTSkEwFXwKBgHGcNg7jNzz3fxJ5wvMySkpV1OgKAJ+lAmhEoJ0ebet9k7F5Fnoe
|
||||
7ibWaURrqNKoQF6lix4g9oIfWgOJv0IpCRgm3EMx2+ugsg1bojZ9Ew2gGRApw0vv
|
||||
AFZi9xBgwWwwZ9ElSLT3P+T6WqYw9tIfDUIa1/pnBTBkwvGg9suNz/1FAoGANNNT
|
||||
Tvk3NpemHrOB6oh0ExqCeW1qUxSTErnbWxv1vf0aNzFd0TxTJ4+mn3sf+C3QUlMv
|
||||
YPMjNQNK+Cq/eVG0LIGbMd37LcXVZ3AOuRXESWaS3PEHxI1fyubqB5m2mLpZU7XH
|
||||
reFfO6PUKLaRs7OtmzRmSUZbwd54rDVuf2SfwscCgYBWb2b0LOBq87ULlI/RnId1
|
||||
DWu1tWop2DXkIAeotpL+yB6S0x9nNi7DbKR9wXOK52EkNx29ky/rJR91jI5IlxBL
|
||||
ydGuZpPL8ZP9WKQtRj16PgQv1HNN8u34MTnBHq6G1tTA0VOhgCS9eZel5EKAZluZ
|
||||
BzsnA+XhRTo2DUyO1IYpnQ==
|
||||
-----END PRIVATE KEY-----
|
||||
@ -1,55 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCZZopOBlV6EuXP
|
||||
uveR+xFnvads+VkCDjjBZWty8rbfr2qbh1Tycq2eYRVt5avb1uQRc0yR6U4U9BX/
|
||||
PTthb3xD9OEQEyCqlJCvCP0Z8MjgKAygeD1HV6hrH/tajL2r93OTGt30RljwU3vq
|
||||
5nvjXnJWjV86k1AKQhnPJmBw7oAt7NJGbR5fmTCmfsTjXYUQwmtfCqNCn7OqGzDy
|
||||
vH6mHa0WHed7j1wgJzV6EAFzU+u0C/7jXjCcQpE/7VefK+Hh9J9alPaVzEeFJjBz
|
||||
7PBxuO0nZOuy1IWOrCQEk10Ad3ynUwbe9qC7EoFGeQaUlEGl5NJrYri+awWBJ+x4
|
||||
Ua9SYWALAgMBAAECggEAArg5YH7zpsyzkiMmJRdQUvufGCsz8EZ9rBp9Tq+0UH/r
|
||||
W6MisILm+bIn72eNTFvH3FiKdXwvT02XUqVskMj2T4m4ycOrcxeO/x0HyINz1EGL
|
||||
HV2awutWoY7GGPAto3rC6+TW8lZvvwcD4Rsn/CGrB+AAXbX9F8l3ORLczkCN0wKW
|
||||
TEuRWZ8gPfZ4Jp84ST0B//qcy6Se9Hb+03k8VC9WJWtoE5uT/Wn4M0yEfdVENdWk
|
||||
7tjr7WRuwWz+PS5ydu9reAOJV0cHJj34NdoFdwXf4Jf5+sngI4meRhhF+8T/ezHA
|
||||
/vY3+KaY7QCfjPcncTc6BM+GV8V2sVzPbz7BuGjxdQKBgQDVlOpWaZIC2sqAtRmV
|
||||
krnHeJTKwmLPlPoZED4q88Bcb4a87F7fHnhQOVajBEaSqBWcbBRD+LhQWdr+3CmX
|
||||
teJqJFLg6oOPvOSpJY7/+QU2LltefoYPTlHjwIEm2lV/10IyqceLnuWDi4e34m2S
|
||||
fljKcemGBJgQWJ2qruFJQWAY1QKBgQC33dk6ZvOqcC1VgB+71Z2j4ZD5S6+SkGRi
|
||||
OwIJ+F7fXgvw+/nH/HhnscVVuDuKjewf3Mg+k3cCHTs0FSaLs6o2N9JBd6VWlG4z
|
||||
HSN13u0FrmZtEOjXGuJS1jRu8LUGFx0xB1/VfGDRT9/B7irprjJfLCOy5FiYTlkJ
|
||||
QqTSkEwFXwKBgHGcNg7jNzz3fxJ5wvMySkpV1OgKAJ+lAmhEoJ0ebet9k7F5Fnoe
|
||||
7ibWaURrqNKoQF6lix4g9oIfWgOJv0IpCRgm3EMx2+ugsg1bojZ9Ew2gGRApw0vv
|
||||
AFZi9xBgwWwwZ9ElSLT3P+T6WqYw9tIfDUIa1/pnBTBkwvGg9suNz/1FAoGANNNT
|
||||
Tvk3NpemHrOB6oh0ExqCeW1qUxSTErnbWxv1vf0aNzFd0TxTJ4+mn3sf+C3QUlMv
|
||||
YPMjNQNK+Cq/eVG0LIGbMd37LcXVZ3AOuRXESWaS3PEHxI1fyubqB5m2mLpZU7XH
|
||||
reFfO6PUKLaRs7OtmzRmSUZbwd54rDVuf2SfwscCgYBWb2b0LOBq87ULlI/RnId1
|
||||
DWu1tWop2DXkIAeotpL+yB6S0x9nNi7DbKR9wXOK52EkNx29ky/rJR91jI5IlxBL
|
||||
ydGuZpPL8ZP9WKQtRj16PgQv1HNN8u34MTnBHq6G1tTA0VOhgCS9eZel5EKAZluZ
|
||||
BzsnA+XhRTo2DUyO1IYpnQ==
|
||||
-----END PRIVATE KEY-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEqDCCA5CgAwIBAgIUckN8Se/RK/askfiTUwBPzwNzehAwDQYJKoZIhvcNAQEL
|
||||
BQAwgYUxCzAJBgNVBAYTAkNBMQswCQYDVQQIDAJPTjEPMA0GA1UEBwwGQmFycmll
|
||||
MRQwEgYDVQQKDAtBRy1OQVYgSW5jLjELMAkGA1UECwwCU0QxEjAQBgNVBAMMCWFn
|
||||
bmF2LmNvbTEhMB8GCSqGSIb3DQEJARYSc29mdHdhcmVAYWduYXYuY29tMB4XDTI1
|
||||
MDEyNzIwMjk0M1oXDTQ1MDEyMjIwMjk0M1owgYwxCzAJBgNVBAYTAkNBMQswCQYD
|
||||
VQQIDAJPTjEPMA0GA1UEBwwGQmFycmllMRQwEgYDVQQKDAtBRy1OQVYgSW5jLjEL
|
||||
MAkGA1UECwwCU0QxGTAXBgNVBAMMEGFnbmRiMC5hZ25hdi5jb20xITAfBgkqhkiG
|
||||
9w0BCQEWEnNvZnR3YXJlQGFnbmF2LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEP
|
||||
ADCCAQoCggEBAJlmik4GVXoS5c+695H7EWe9p2z5WQIOOMFla3Lytt+vapuHVPJy
|
||||
rZ5hFW3lq9vW5BFzTJHpThT0Ff89O2FvfEP04RATIKqUkK8I/RnwyOAoDKB4PUdX
|
||||
qGsf+1qMvav3c5Ma3fRGWPBTe+rme+NeclaNXzqTUApCGc8mYHDugC3s0kZtHl+Z
|
||||
MKZ+xONdhRDCa18Ko0Kfs6obMPK8fqYdrRYd53uPXCAnNXoQAXNT67QL/uNeMJxC
|
||||
kT/tV58r4eH0n1qU9pXMR4UmMHPs8HG47Sdk67LUhY6sJASTXQB3fKdTBt72oLsS
|
||||
gUZ5BpSUQaXk0mtiuL5rBYEn7HhRr1JhYAsCAwEAAaOCAQUwggEBMIGvBgNVHSME
|
||||
gacwgaShgYukgYgwgYUxCzAJBgNVBAYTAkNBMQswCQYDVQQIDAJPTjEPMA0GA1UE
|
||||
BwwGQmFycmllMRQwEgYDVQQKDAtBRy1OQVYgSW5jLjELMAkGA1UECwwCU0QxEjAQ
|
||||
BgNVBAMMCWFnbmF2LmNvbTEhMB8GCSqGSIb3DQEJARYSc29mdHdhcmVAYWduYXYu
|
||||
Y29tghRSFuD2DW20Rsy92FMPr8mi5Xi1wDAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE
|
||||
8DAWBgNVHREEDzANggsqLmFnbmF2LmNvbTAdBgNVHQ4EFgQUTZxBYBMPCDnCf6zx
|
||||
eK9cPLAqYUgwDQYJKoZIhvcNAQELBQADggEBADeFFlZiKeAkKsqeCyBYa70iv8lB
|
||||
54bzIIjdvQUctDkfjkmZo5rwsGvtwD8peNFegKy7BiXzDZYJ0ZlAAEZ5bUazjnYS
|
||||
SHEqid8WVeCnsgp9TWi8uMX4BaVTJr2LvSsYF7SrlCTx+aRPLes2CP3FPXSFo81K
|
||||
zs9p9mPAHPbtByHb/CIkwyh2HX4Ap0SDYGAOVf9mC7sDWfDWTiIWSAl4YbAVily9
|
||||
E1JqwPMH8aHNBOTpXZPYto4TIrF4vVIeXP/EVrKVfbeZMNwNFfrRqWcAPMAXX3hE
|
||||
XTFzhFeepdXExckC6ulNmfLaitxLp9L0J8NqCzrLC2WIoau5CXqGR1Ux7FY=
|
||||
-----END CERTIFICATE-----
|
||||
@ -1,27 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEqDCCA5CgAwIBAgIUckN8Se/RK/askfiTUwBPzwNzehEwDQYJKoZIhvcNAQEL
|
||||
BQAwgYUxCzAJBgNVBAYTAkNBMQswCQYDVQQIDAJPTjEPMA0GA1UEBwwGQmFycmll
|
||||
MRQwEgYDVQQKDAtBRy1OQVYgSW5jLjELMAkGA1UECwwCU0QxEjAQBgNVBAMMCWFn
|
||||
bmF2LmNvbTEhMB8GCSqGSIb3DQEJARYSc29mdHdhcmVAYWduYXYuY29tMB4XDTI1
|
||||
MDEyNzIwMzE1N1oXDTQ1MDEyMjIwMzE1N1owgYwxCzAJBgNVBAYTAkNBMQswCQYD
|
||||
VQQIDAJPTjEPMA0GA1UEBwwGQmFycmllMRQwEgYDVQQKDAtBRy1OQVYgSW5jLjEL
|
||||
MAkGA1UECwwCU0QxGTAXBgNVBAMMEGFnbmRiMS5hZ25hdi5jb20xITAfBgkqhkiG
|
||||
9w0BCQEWEnNvZnR3YXJlQGFnbmF2LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEP
|
||||
ADCCAQoCggEBAIzS5Y3k75MspjIKthkwfoGyOGYToCd3kTdXgk/rt4qFxOWdaTPp
|
||||
4cHlQeYg7MzXDBMU3PKi7VO2rqGyzHU9bBY/srT4G3zPoeggU+UgOLx5FPzJhJzb
|
||||
XiP/9V61JKjwzmKsCkH/0cozjJOKQQAEqXm4FRa6McFX1DR607Huc6lKJ6Rk3GP5
|
||||
MQCLEW844ZRet2kc2LXnoZ5RBkEQb6WGIiwyg/lys/qz6niQnT+ok5xTiIQ7b+lH
|
||||
yZu0vc+dE1tWg/8MWCPgIAfhIPLxmdhxMR0I3X+bGQIBefNKLTMBfRl0GkT15mgM
|
||||
rMNNOW8T5/DmijJwtoS9dV7RBxkMOWLnnPcCAwEAAaOCAQUwggEBMIGvBgNVHSME
|
||||
gacwgaShgYukgYgwgYUxCzAJBgNVBAYTAkNBMQswCQYDVQQIDAJPTjEPMA0GA1UE
|
||||
BwwGQmFycmllMRQwEgYDVQQKDAtBRy1OQVYgSW5jLjELMAkGA1UECwwCU0QxEjAQ
|
||||
BgNVBAMMCWFnbmF2LmNvbTEhMB8GCSqGSIb3DQEJARYSc29mdHdhcmVAYWduYXYu
|
||||
Y29tghRSFuD2DW20Rsy92FMPr8mi5Xi1wDAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE
|
||||
8DAWBgNVHREEDzANggsqLmFnbmF2LmNvbTAdBgNVHQ4EFgQU43FtisxQn6v/AHgb
|
||||
fqXp8WcaqvQwDQYJKoZIhvcNAQELBQADggEBAID9NGobJSKUQOxbIEMXojOs5cnQ
|
||||
Rj70ZrnyxgUA+bqBfBSocN77KufvCyXgLAIfcD0di82VFyjkB8c69X2gZbzXeYJA
|
||||
pXXsEPCTDNzUDduyD9AF95fNKTnxQtQObs1E5aq2mmzE+I09rF+mnXwmehe3mkmY
|
||||
YBIlrokHiaIEQo1t760pFRLCwiRnXNd61xP/d+i3ARLeRD0bPy/PHerEN9uc9wO0
|
||||
i1HRnDO5LLWDgbgz+JUP4ImTyVphdYcgFngO1hvK5YfZ/j6uo1ynp9g0ff9UPAiR
|
||||
HSvIWuIxx+WQlS6e22+fHQcfdGVjVzW9oPoMZ9pgKPiGU0HdxwPXLuZbfc8=
|
||||
-----END CERTIFICATE-----
|
||||
@ -1,18 +0,0 @@
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIC0jCCAboCAQAwgYwxCzAJBgNVBAYTAkNBMQswCQYDVQQIDAJPTjEPMA0GA1UE
|
||||
BwwGQmFycmllMRQwEgYDVQQKDAtBRy1OQVYgSW5jLjELMAkGA1UECwwCU0QxGTAX
|
||||
BgNVBAMMEGFnbmRiMS5hZ25hdi5jb20xITAfBgkqhkiG9w0BCQEWEnNvZnR3YXJl
|
||||
QGFnbmF2LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIzS5Y3k
|
||||
75MspjIKthkwfoGyOGYToCd3kTdXgk/rt4qFxOWdaTPp4cHlQeYg7MzXDBMU3PKi
|
||||
7VO2rqGyzHU9bBY/srT4G3zPoeggU+UgOLx5FPzJhJzbXiP/9V61JKjwzmKsCkH/
|
||||
0cozjJOKQQAEqXm4FRa6McFX1DR607Huc6lKJ6Rk3GP5MQCLEW844ZRet2kc2LXn
|
||||
oZ5RBkEQb6WGIiwyg/lys/qz6niQnT+ok5xTiIQ7b+lHyZu0vc+dE1tWg/8MWCPg
|
||||
IAfhIPLxmdhxMR0I3X+bGQIBefNKLTMBfRl0GkT15mgMrMNNOW8T5/DmijJwtoS9
|
||||
dV7RBxkMOWLnnPcCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQB4NQv1KAUTG067
|
||||
MxJkzRkr2+nXcx2iEvDpHlrUtXIDbqUYnd2VwqzMnWtvK4nssD158LEoKhQkyFX7
|
||||
6bOYc3DCl8AmObSlY8UBdr85iX8w/oFpoQKJ74mwsa2dTaIRY/h9zD8xnPsU3mCl
|
||||
+M1p2/9A2vNLFuiOjWfxauK2PPmYltUnMCTvVJNZhlmiYZb1D3Ot7omvFriVzP6l
|
||||
8ZCAuvZ0wWKaU96cx0xxOpAZur7vuYfW68LaBRBivgJD3/gO/E2myb76qOjulmKc
|
||||
rPo3dtc36hfMGXTbb9hn5XNG0u+2OjteKmQRcTWf07yopT8PhAZZ8YwR4PC8R30c
|
||||
1LRXvWsj
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
@ -1,28 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQCM0uWN5O+TLKYy
|
||||
CrYZMH6BsjhmE6And5E3V4JP67eKhcTlnWkz6eHB5UHmIOzM1wwTFNzyou1Ttq6h
|
||||
ssx1PWwWP7K0+Bt8z6HoIFPlIDi8eRT8yYSc214j//VetSSo8M5irApB/9HKM4yT
|
||||
ikEABKl5uBUWujHBV9Q0etOx7nOpSiekZNxj+TEAixFvOOGUXrdpHNi156GeUQZB
|
||||
EG+lhiIsMoP5crP6s+p4kJ0/qJOcU4iEO2/pR8mbtL3PnRNbVoP/DFgj4CAH4SDy
|
||||
8ZnYcTEdCN1/mxkCAXnzSi0zAX0ZdBpE9eZoDKzDTTlvE+fw5ooycLaEvXVe0QcZ
|
||||
DDli55z3AgMBAAECgf9EwrVTk3VnZ2W/CXvg0Mz0hxECxpJssvU1EIWQgIgeGzMI
|
||||
6tNzhb+17TtAItN9MYOzzXwU/XjkZ07dpXAvzs34DyIzOvIw/zEPJrnQJuuNG5Ij
|
||||
3EHkVTiiW1hz5f8AfpRHmblogIDvOjxpzhfu6lfWFww1DLU+sqPq/A5h0wymm9U0
|
||||
tv7zcgKl/C9yYBfKPHzG0lS8ejIKEajFvcvXH3m6jvG+LEhzJWP9luy6nVmiZE6K
|
||||
WKnRn2+UmOWEpsYAmqfDk9MwsMaagpjoYyzh18P+rHa4Eshalgk2dLxJN05sCwqJ
|
||||
onDTRy0Bvxm+vO9iYJVDl5Elrz7XuwNPNExg5YkCgYEAxNzDPK3X95Dl9SI5Cwi/
|
||||
jj5sPAemajEd3Dqjj8kI6w+xwDg8rPHH7RvY76vpSUkQpUzhrRX2bzXXm/weUr5n
|
||||
pRAdDdrTHO78GTBa8q/TAyRpdlXd5oVCjJQyY30Yih8d5edwr8vtWlyZzXWFotNx
|
||||
5vPM0GocZBvYh946ikfEoEsCgYEAtyCfKZymbHGzyX8NEZlIk6Ljl3E1EYEjZb3s
|
||||
u9qbSdjZf43FVaGkovhn+r+y1yCW+eILhQwTZDi9b2nsv0Yk0eQ3z+GYWILOF0nJ
|
||||
rsxrs2x1gPuVQf8QDIDmvfNzAAHQIsq6gXGwukQLRA5dxeN4zPB1JBP4U8QLOYQa
|
||||
KtcnQoUCgYEAh7gjKaw4XkcZIp0Lcp3/YhOLDv+/LSrbiT8sEC7q5ROW2gxrWFgA
|
||||
G2m9b863MH0c6rlMRMYFdbpLAREZ3rXCQrwPK8QXE7V3O+5oZTPuaBYsVxbvusNY
|
||||
lA5/hrNxvZeiRyP+PlR7OHbq2gkRrqXTuwONyom9NQ81gsYk2byMxG0CgYAYM3p1
|
||||
UFt6F2iwJ1c9zSkXQb6cI/zkbFGWP4xKozBEiSDtR3odv/f3BacQL0deQNNTALmP
|
||||
ArKJWypF0BTWjlmNV4C8u06b2+WKlFjP/fn5w0qgGh92klO3o01bKxI2nQa5olsV
|
||||
gkXdx+JJQzDHVzF+vARvGSiHQXBOUJP4t2hb1QKBgEMXaOEGoffA77akNE7wdDuN
|
||||
yDTORLtB4pcQb1CjICrDyxmlK3xbejonkR+WvizoHQQ0DJK45zUtwErTMyRvtc+Q
|
||||
bR9w41DDhgixArH7wSbnw4CfFq7X/m4jb8sQBLHk60MH2kih0RRrHktXBNtRXGv2
|
||||
op+mE1ucT/ZC6F/dwadJ
|
||||
-----END PRIVATE KEY-----
|
||||
@ -1,55 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQCM0uWN5O+TLKYy
|
||||
CrYZMH6BsjhmE6And5E3V4JP67eKhcTlnWkz6eHB5UHmIOzM1wwTFNzyou1Ttq6h
|
||||
ssx1PWwWP7K0+Bt8z6HoIFPlIDi8eRT8yYSc214j//VetSSo8M5irApB/9HKM4yT
|
||||
ikEABKl5uBUWujHBV9Q0etOx7nOpSiekZNxj+TEAixFvOOGUXrdpHNi156GeUQZB
|
||||
EG+lhiIsMoP5crP6s+p4kJ0/qJOcU4iEO2/pR8mbtL3PnRNbVoP/DFgj4CAH4SDy
|
||||
8ZnYcTEdCN1/mxkCAXnzSi0zAX0ZdBpE9eZoDKzDTTlvE+fw5ooycLaEvXVe0QcZ
|
||||
DDli55z3AgMBAAECgf9EwrVTk3VnZ2W/CXvg0Mz0hxECxpJssvU1EIWQgIgeGzMI
|
||||
6tNzhb+17TtAItN9MYOzzXwU/XjkZ07dpXAvzs34DyIzOvIw/zEPJrnQJuuNG5Ij
|
||||
3EHkVTiiW1hz5f8AfpRHmblogIDvOjxpzhfu6lfWFww1DLU+sqPq/A5h0wymm9U0
|
||||
tv7zcgKl/C9yYBfKPHzG0lS8ejIKEajFvcvXH3m6jvG+LEhzJWP9luy6nVmiZE6K
|
||||
WKnRn2+UmOWEpsYAmqfDk9MwsMaagpjoYyzh18P+rHa4Eshalgk2dLxJN05sCwqJ
|
||||
onDTRy0Bvxm+vO9iYJVDl5Elrz7XuwNPNExg5YkCgYEAxNzDPK3X95Dl9SI5Cwi/
|
||||
jj5sPAemajEd3Dqjj8kI6w+xwDg8rPHH7RvY76vpSUkQpUzhrRX2bzXXm/weUr5n
|
||||
pRAdDdrTHO78GTBa8q/TAyRpdlXd5oVCjJQyY30Yih8d5edwr8vtWlyZzXWFotNx
|
||||
5vPM0GocZBvYh946ikfEoEsCgYEAtyCfKZymbHGzyX8NEZlIk6Ljl3E1EYEjZb3s
|
||||
u9qbSdjZf43FVaGkovhn+r+y1yCW+eILhQwTZDi9b2nsv0Yk0eQ3z+GYWILOF0nJ
|
||||
rsxrs2x1gPuVQf8QDIDmvfNzAAHQIsq6gXGwukQLRA5dxeN4zPB1JBP4U8QLOYQa
|
||||
KtcnQoUCgYEAh7gjKaw4XkcZIp0Lcp3/YhOLDv+/LSrbiT8sEC7q5ROW2gxrWFgA
|
||||
G2m9b863MH0c6rlMRMYFdbpLAREZ3rXCQrwPK8QXE7V3O+5oZTPuaBYsVxbvusNY
|
||||
lA5/hrNxvZeiRyP+PlR7OHbq2gkRrqXTuwONyom9NQ81gsYk2byMxG0CgYAYM3p1
|
||||
UFt6F2iwJ1c9zSkXQb6cI/zkbFGWP4xKozBEiSDtR3odv/f3BacQL0deQNNTALmP
|
||||
ArKJWypF0BTWjlmNV4C8u06b2+WKlFjP/fn5w0qgGh92klO3o01bKxI2nQa5olsV
|
||||
gkXdx+JJQzDHVzF+vARvGSiHQXBOUJP4t2hb1QKBgEMXaOEGoffA77akNE7wdDuN
|
||||
yDTORLtB4pcQb1CjICrDyxmlK3xbejonkR+WvizoHQQ0DJK45zUtwErTMyRvtc+Q
|
||||
bR9w41DDhgixArH7wSbnw4CfFq7X/m4jb8sQBLHk60MH2kih0RRrHktXBNtRXGv2
|
||||
op+mE1ucT/ZC6F/dwadJ
|
||||
-----END PRIVATE KEY-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEqDCCA5CgAwIBAgIUckN8Se/RK/askfiTUwBPzwNzehEwDQYJKoZIhvcNAQEL
|
||||
BQAwgYUxCzAJBgNVBAYTAkNBMQswCQYDVQQIDAJPTjEPMA0GA1UEBwwGQmFycmll
|
||||
MRQwEgYDVQQKDAtBRy1OQVYgSW5jLjELMAkGA1UECwwCU0QxEjAQBgNVBAMMCWFn
|
||||
bmF2LmNvbTEhMB8GCSqGSIb3DQEJARYSc29mdHdhcmVAYWduYXYuY29tMB4XDTI1
|
||||
MDEyNzIwMzE1N1oXDTQ1MDEyMjIwMzE1N1owgYwxCzAJBgNVBAYTAkNBMQswCQYD
|
||||
VQQIDAJPTjEPMA0GA1UEBwwGQmFycmllMRQwEgYDVQQKDAtBRy1OQVYgSW5jLjEL
|
||||
MAkGA1UECwwCU0QxGTAXBgNVBAMMEGFnbmRiMS5hZ25hdi5jb20xITAfBgkqhkiG
|
||||
9w0BCQEWEnNvZnR3YXJlQGFnbmF2LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEP
|
||||
ADCCAQoCggEBAIzS5Y3k75MspjIKthkwfoGyOGYToCd3kTdXgk/rt4qFxOWdaTPp
|
||||
4cHlQeYg7MzXDBMU3PKi7VO2rqGyzHU9bBY/srT4G3zPoeggU+UgOLx5FPzJhJzb
|
||||
XiP/9V61JKjwzmKsCkH/0cozjJOKQQAEqXm4FRa6McFX1DR607Huc6lKJ6Rk3GP5
|
||||
MQCLEW844ZRet2kc2LXnoZ5RBkEQb6WGIiwyg/lys/qz6niQnT+ok5xTiIQ7b+lH
|
||||
yZu0vc+dE1tWg/8MWCPgIAfhIPLxmdhxMR0I3X+bGQIBefNKLTMBfRl0GkT15mgM
|
||||
rMNNOW8T5/DmijJwtoS9dV7RBxkMOWLnnPcCAwEAAaOCAQUwggEBMIGvBgNVHSME
|
||||
gacwgaShgYukgYgwgYUxCzAJBgNVBAYTAkNBMQswCQYDVQQIDAJPTjEPMA0GA1UE
|
||||
BwwGQmFycmllMRQwEgYDVQQKDAtBRy1OQVYgSW5jLjELMAkGA1UECwwCU0QxEjAQ
|
||||
BgNVBAMMCWFnbmF2LmNvbTEhMB8GCSqGSIb3DQEJARYSc29mdHdhcmVAYWduYXYu
|
||||
Y29tghRSFuD2DW20Rsy92FMPr8mi5Xi1wDAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE
|
||||
8DAWBgNVHREEDzANggsqLmFnbmF2LmNvbTAdBgNVHQ4EFgQU43FtisxQn6v/AHgb
|
||||
fqXp8WcaqvQwDQYJKoZIhvcNAQELBQADggEBAID9NGobJSKUQOxbIEMXojOs5cnQ
|
||||
Rj70ZrnyxgUA+bqBfBSocN77KufvCyXgLAIfcD0di82VFyjkB8c69X2gZbzXeYJA
|
||||
pXXsEPCTDNzUDduyD9AF95fNKTnxQtQObs1E5aq2mmzE+I09rF+mnXwmehe3mkmY
|
||||
YBIlrokHiaIEQo1t760pFRLCwiRnXNd61xP/d+i3ARLeRD0bPy/PHerEN9uc9wO0
|
||||
i1HRnDO5LLWDgbgz+JUP4ImTyVphdYcgFngO1hvK5YfZ/j6uo1ynp9g0ff9UPAiR
|
||||
HSvIWuIxx+WQlS6e22+fHQcfdGVjVzW9oPoMZ9pgKPiGU0HdxwPXLuZbfc8=
|
||||
-----END CERTIFICATE-----
|
||||
@ -1,27 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEqDCCA5CgAwIBAgIUckN8Se/RK/askfiTUwBPzwNzehIwDQYJKoZIhvcNAQEL
|
||||
BQAwgYUxCzAJBgNVBAYTAkNBMQswCQYDVQQIDAJPTjEPMA0GA1UEBwwGQmFycmll
|
||||
MRQwEgYDVQQKDAtBRy1OQVYgSW5jLjELMAkGA1UECwwCU0QxEjAQBgNVBAMMCWFn
|
||||
bmF2LmNvbTEhMB8GCSqGSIb3DQEJARYSc29mdHdhcmVAYWduYXYuY29tMB4XDTI1
|
||||
MDEyNzIwMzIzNloXDTQ1MDEyMjIwMzIzNlowgYwxCzAJBgNVBAYTAkNBMQswCQYD
|
||||
VQQIDAJPTjEPMA0GA1UEBwwGQmFycmllMRQwEgYDVQQKDAtBRy1OQVYgSW5jLjEL
|
||||
MAkGA1UECwwCU0QxGTAXBgNVBAMMEGFnbmRiMi5hZ25hdi5jb20xITAfBgkqhkiG
|
||||
9w0BCQEWEnNvZnR3YXJlQGFnbmF2LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEP
|
||||
ADCCAQoCggEBAKR9efW8IVVjHyON3PJZuIB0Y3NE8VBGtW60XIQvXPlW2CqLVAMB
|
||||
f9ZDsJ0jqkrSouo5X14WkMRj1n/Fi1vzBWBx3vqvJNHr5Idcoq+0V/R06ggHzKpn
|
||||
smXoAMls8Nm/uPN+PUuTUINY0JiYm0WNTjHkqiZyqnGOb/3cE3I9Lfb34uqFece6
|
||||
w06aqkUzc2BZ1ME3xaauZ5L60XaWU2gU2z1sYywh4kyNEyugjLwhrugyElQYAgvE
|
||||
6EofE3ds6cIjt1Z5gQauN2PSv1D1tIbXtY8b5xN3Zx5AT7CkXdGXB5vltjgq3UX/
|
||||
6w6ZIB3g6F/CnOd/9FlX8v1pWLKXGys9BncCAwEAAaOCAQUwggEBMIGvBgNVHSME
|
||||
gacwgaShgYukgYgwgYUxCzAJBgNVBAYTAkNBMQswCQYDVQQIDAJPTjEPMA0GA1UE
|
||||
BwwGQmFycmllMRQwEgYDVQQKDAtBRy1OQVYgSW5jLjELMAkGA1UECwwCU0QxEjAQ
|
||||
BgNVBAMMCWFnbmF2LmNvbTEhMB8GCSqGSIb3DQEJARYSc29mdHdhcmVAYWduYXYu
|
||||
Y29tghRSFuD2DW20Rsy92FMPr8mi5Xi1wDAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE
|
||||
8DAWBgNVHREEDzANggsqLmFnbmF2LmNvbTAdBgNVHQ4EFgQUvAyRGjZQeVhXnSH7
|
||||
r0uvB1iav7AwDQYJKoZIhvcNAQELBQADggEBAFLyAC7HhYoae2KPz95H+ZPqRnpV
|
||||
1BjpuZ+jscADWlGWvwIvfx5a4vKZf/Fj+uhqcbCU4OCHRqj2qWdWeHRlO/uDDFCm
|
||||
LahRHaBYvSpnljQhlsuYLOCDii0Fz93tvnZjhM633p6p+yFkZ5jgVZ+7jJDhqXwj
|
||||
RF2j61diRHlIIIok9KFZlrPOjtWdfKaaFMsPMQlp+vhpVa6pEBGtTtMQfuJl1puM
|
||||
rA3NtKkyCVgF8iuNrLQzyGGy2WOShhxz+U830HcYAkha6ye0fU8FXXdKFTXZd0p/
|
||||
neu8iXLKuiGvnsMe1/E92r1uFNmmwQCpCjGNstV2rpoOmHVaB9MQ5YUEEVM=
|
||||
-----END CERTIFICATE-----
|
||||
@ -1,18 +0,0 @@
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIC0jCCAboCAQAwgYwxCzAJBgNVBAYTAkNBMQswCQYDVQQIDAJPTjEPMA0GA1UE
|
||||
BwwGQmFycmllMRQwEgYDVQQKDAtBRy1OQVYgSW5jLjELMAkGA1UECwwCU0QxGTAX
|
||||
BgNVBAMMEGFnbmRiMi5hZ25hdi5jb20xITAfBgkqhkiG9w0BCQEWEnNvZnR3YXJl
|
||||
QGFnbmF2LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKR9efW8
|
||||
IVVjHyON3PJZuIB0Y3NE8VBGtW60XIQvXPlW2CqLVAMBf9ZDsJ0jqkrSouo5X14W
|
||||
kMRj1n/Fi1vzBWBx3vqvJNHr5Idcoq+0V/R06ggHzKpnsmXoAMls8Nm/uPN+PUuT
|
||||
UINY0JiYm0WNTjHkqiZyqnGOb/3cE3I9Lfb34uqFece6w06aqkUzc2BZ1ME3xaau
|
||||
Z5L60XaWU2gU2z1sYywh4kyNEyugjLwhrugyElQYAgvE6EofE3ds6cIjt1Z5gQau
|
||||
N2PSv1D1tIbXtY8b5xN3Zx5AT7CkXdGXB5vltjgq3UX/6w6ZIB3g6F/CnOd/9FlX
|
||||
8v1pWLKXGys9BncCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQB7nuxP9I2AQhEe
|
||||
5B3QTC5bR1NoynoWmggOBZpOmiHyNSZxtObTUqrPdVJf/fHLDDH5clXPZAFn2WhU
|
||||
WBV8Z8+F7ibARpv/wEA9tNbiJJlLd4E478qOxwsEu0UFnWRS+uLjSmhCv4TGYTem
|
||||
ZfXLe3SZju0zQIZytYIwfzM9DYIIlJi4YozorCGdoKsLVyF4HZS1UAFGmdSDn6Nq
|
||||
fFE24Z5Y15QtX1OUlD4tYe6cXDmeUNPQdZYnofz+WJjcF5hJUUWUbrpQVavjW7x6
|
||||
uW6sL9YNuS8gayILkATE+u5pQq+pExrTFPFE9JHsmGpXSflt2VUHj20yFFVBleS6
|
||||
mN1ZCa6S
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
@ -1,28 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCkfXn1vCFVYx8j
|
||||
jdzyWbiAdGNzRPFQRrVutFyEL1z5Vtgqi1QDAX/WQ7CdI6pK0qLqOV9eFpDEY9Z/
|
||||
xYtb8wVgcd76ryTR6+SHXKKvtFf0dOoIB8yqZ7Jl6ADJbPDZv7jzfj1Lk1CDWNCY
|
||||
mJtFjU4x5Komcqpxjm/93BNyPS329+LqhXnHusNOmqpFM3NgWdTBN8WmrmeS+tF2
|
||||
llNoFNs9bGMsIeJMjRMroIy8Ia7oMhJUGAILxOhKHxN3bOnCI7dWeYEGrjdj0r9Q
|
||||
9bSG17WPG+cTd2ceQE+wpF3Rlweb5bY4Kt1F/+sOmSAd4Ohfwpznf/RZV/L9aViy
|
||||
lxsrPQZ3AgMBAAECggEACo4ovN2hL/cH4QKx+nVKcyfE1/lFGPconl2BkFPvvepW
|
||||
PciqD/VRLEE0vKi0vU9t9+TNm16MVKXpdlKJYlGidzFncyO5E6V1CUCnCepjOIMm
|
||||
H3TMq+SynHp6TnKos/hrGGjx6v8djAgvfpNQZ3KtYWNAje4OxPG0exBbyRlm8TGV
|
||||
i/kjTJdVVWvisNuAr6cbQOS7qp/0ZGZbTm0SnFBfxDMbYj04g/5Eoz/QfwRHYjXf
|
||||
hGd4kZay/T8HiU0d/E49tgY+pEip1ZeltVXcvXPGQiJfPtB0zHrB7pzClqPDK5zO
|
||||
qluEsqFpL6XiTDhbwbvJcrGoOg9z62uGz68nc52JDQKBgQDjDK9wjEu4wLxb9/Zz
|
||||
da2bSfKBdRHBkWjy60R9u1Gls4OE+X7RloJloP91ez/ItNmcFaTp9BMerq9yeFXA
|
||||
dWvFsX5JaH3cqH5lbU2LaplhRepfdlEql8W4belLo4RG8NRAk+phbdS38iqdU++f
|
||||
eU5kLQm+CxEOoiqlX/fBApWHtQKBgQC5dryqgBcN6sb0US7nrg5jKeb4kXLT8YPD
|
||||
xgIeozylLLim1+4qqQczP01gQANyNVa2kcj0547u5R8L9cSCXHC4zbz+yqNGWrH8
|
||||
RKc2ZFDaJoTAAESqdxrnh3mHhKStC17AOyTf6PaGiJFVV09N00bE44texdmwC0q9
|
||||
O26dh0gY+wKBgER3x9QFyrPdEzAct2ob+41PoFyfpAoeQmq2vcG4oid42dlYr4Ce
|
||||
hZYGFeMklph8yP6DnGacnsNq5Cd92EYgYq6MFswmQYYwHWWSr4ayT6yAu+urr1BL
|
||||
0mkKZAbWOYZ3C1qqAEz/JN2PnoAxFpqdpyEYX866YZtrFbcnGHxQvizdAoGACnCF
|
||||
lC9bnNvvUQdU1ZO1mZ1dM/az+PwqR5XYvrK/kiifSDz1Wg9jqV3R1C4mQ4J/HA2+
|
||||
uxJhuE7LXZf69L+RVMW38rujTy2BwUp4AxbIek8av9gEBXho2kmE7LzprBfswHNT
|
||||
0wrA/beoPp6Ihz/yRtjsGmyWoVMxZM94nYNk6osCgYAwsaBrs6mrP+O+mmnMVaxV
|
||||
BBtsHtmw8CM6ZvcmgkpB9soSyCoqVJ/2uhRSMSpX/5PpH7/hJvxJUFPoZrEkQqVm
|
||||
WnYp7ipuNwSH6XXNlr31etCX0VFIbedAB7kp+hUZYmW61ZKk1NkyP/E3mUA7J3x3
|
||||
RApxvwKRCebUik0wQEfBpA==
|
||||
-----END PRIVATE KEY-----
|
||||
@ -1,55 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCkfXn1vCFVYx8j
|
||||
jdzyWbiAdGNzRPFQRrVutFyEL1z5Vtgqi1QDAX/WQ7CdI6pK0qLqOV9eFpDEY9Z/
|
||||
xYtb8wVgcd76ryTR6+SHXKKvtFf0dOoIB8yqZ7Jl6ADJbPDZv7jzfj1Lk1CDWNCY
|
||||
mJtFjU4x5Komcqpxjm/93BNyPS329+LqhXnHusNOmqpFM3NgWdTBN8WmrmeS+tF2
|
||||
llNoFNs9bGMsIeJMjRMroIy8Ia7oMhJUGAILxOhKHxN3bOnCI7dWeYEGrjdj0r9Q
|
||||
9bSG17WPG+cTd2ceQE+wpF3Rlweb5bY4Kt1F/+sOmSAd4Ohfwpznf/RZV/L9aViy
|
||||
lxsrPQZ3AgMBAAECggEACo4ovN2hL/cH4QKx+nVKcyfE1/lFGPconl2BkFPvvepW
|
||||
PciqD/VRLEE0vKi0vU9t9+TNm16MVKXpdlKJYlGidzFncyO5E6V1CUCnCepjOIMm
|
||||
H3TMq+SynHp6TnKos/hrGGjx6v8djAgvfpNQZ3KtYWNAje4OxPG0exBbyRlm8TGV
|
||||
i/kjTJdVVWvisNuAr6cbQOS7qp/0ZGZbTm0SnFBfxDMbYj04g/5Eoz/QfwRHYjXf
|
||||
hGd4kZay/T8HiU0d/E49tgY+pEip1ZeltVXcvXPGQiJfPtB0zHrB7pzClqPDK5zO
|
||||
qluEsqFpL6XiTDhbwbvJcrGoOg9z62uGz68nc52JDQKBgQDjDK9wjEu4wLxb9/Zz
|
||||
da2bSfKBdRHBkWjy60R9u1Gls4OE+X7RloJloP91ez/ItNmcFaTp9BMerq9yeFXA
|
||||
dWvFsX5JaH3cqH5lbU2LaplhRepfdlEql8W4belLo4RG8NRAk+phbdS38iqdU++f
|
||||
eU5kLQm+CxEOoiqlX/fBApWHtQKBgQC5dryqgBcN6sb0US7nrg5jKeb4kXLT8YPD
|
||||
xgIeozylLLim1+4qqQczP01gQANyNVa2kcj0547u5R8L9cSCXHC4zbz+yqNGWrH8
|
||||
RKc2ZFDaJoTAAESqdxrnh3mHhKStC17AOyTf6PaGiJFVV09N00bE44texdmwC0q9
|
||||
O26dh0gY+wKBgER3x9QFyrPdEzAct2ob+41PoFyfpAoeQmq2vcG4oid42dlYr4Ce
|
||||
hZYGFeMklph8yP6DnGacnsNq5Cd92EYgYq6MFswmQYYwHWWSr4ayT6yAu+urr1BL
|
||||
0mkKZAbWOYZ3C1qqAEz/JN2PnoAxFpqdpyEYX866YZtrFbcnGHxQvizdAoGACnCF
|
||||
lC9bnNvvUQdU1ZO1mZ1dM/az+PwqR5XYvrK/kiifSDz1Wg9jqV3R1C4mQ4J/HA2+
|
||||
uxJhuE7LXZf69L+RVMW38rujTy2BwUp4AxbIek8av9gEBXho2kmE7LzprBfswHNT
|
||||
0wrA/beoPp6Ihz/yRtjsGmyWoVMxZM94nYNk6osCgYAwsaBrs6mrP+O+mmnMVaxV
|
||||
BBtsHtmw8CM6ZvcmgkpB9soSyCoqVJ/2uhRSMSpX/5PpH7/hJvxJUFPoZrEkQqVm
|
||||
WnYp7ipuNwSH6XXNlr31etCX0VFIbedAB7kp+hUZYmW61ZKk1NkyP/E3mUA7J3x3
|
||||
RApxvwKRCebUik0wQEfBpA==
|
||||
-----END PRIVATE KEY-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEqDCCA5CgAwIBAgIUckN8Se/RK/askfiTUwBPzwNzehIwDQYJKoZIhvcNAQEL
|
||||
BQAwgYUxCzAJBgNVBAYTAkNBMQswCQYDVQQIDAJPTjEPMA0GA1UEBwwGQmFycmll
|
||||
MRQwEgYDVQQKDAtBRy1OQVYgSW5jLjELMAkGA1UECwwCU0QxEjAQBgNVBAMMCWFn
|
||||
bmF2LmNvbTEhMB8GCSqGSIb3DQEJARYSc29mdHdhcmVAYWduYXYuY29tMB4XDTI1
|
||||
MDEyNzIwMzIzNloXDTQ1MDEyMjIwMzIzNlowgYwxCzAJBgNVBAYTAkNBMQswCQYD
|
||||
VQQIDAJPTjEPMA0GA1UEBwwGQmFycmllMRQwEgYDVQQKDAtBRy1OQVYgSW5jLjEL
|
||||
MAkGA1UECwwCU0QxGTAXBgNVBAMMEGFnbmRiMi5hZ25hdi5jb20xITAfBgkqhkiG
|
||||
9w0BCQEWEnNvZnR3YXJlQGFnbmF2LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEP
|
||||
ADCCAQoCggEBAKR9efW8IVVjHyON3PJZuIB0Y3NE8VBGtW60XIQvXPlW2CqLVAMB
|
||||
f9ZDsJ0jqkrSouo5X14WkMRj1n/Fi1vzBWBx3vqvJNHr5Idcoq+0V/R06ggHzKpn
|
||||
smXoAMls8Nm/uPN+PUuTUINY0JiYm0WNTjHkqiZyqnGOb/3cE3I9Lfb34uqFece6
|
||||
w06aqkUzc2BZ1ME3xaauZ5L60XaWU2gU2z1sYywh4kyNEyugjLwhrugyElQYAgvE
|
||||
6EofE3ds6cIjt1Z5gQauN2PSv1D1tIbXtY8b5xN3Zx5AT7CkXdGXB5vltjgq3UX/
|
||||
6w6ZIB3g6F/CnOd/9FlX8v1pWLKXGys9BncCAwEAAaOCAQUwggEBMIGvBgNVHSME
|
||||
gacwgaShgYukgYgwgYUxCzAJBgNVBAYTAkNBMQswCQYDVQQIDAJPTjEPMA0GA1UE
|
||||
BwwGQmFycmllMRQwEgYDVQQKDAtBRy1OQVYgSW5jLjELMAkGA1UECwwCU0QxEjAQ
|
||||
BgNVBAMMCWFnbmF2LmNvbTEhMB8GCSqGSIb3DQEJARYSc29mdHdhcmVAYWduYXYu
|
||||
Y29tghRSFuD2DW20Rsy92FMPr8mi5Xi1wDAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE
|
||||
8DAWBgNVHREEDzANggsqLmFnbmF2LmNvbTAdBgNVHQ4EFgQUvAyRGjZQeVhXnSH7
|
||||
r0uvB1iav7AwDQYJKoZIhvcNAQELBQADggEBAFLyAC7HhYoae2KPz95H+ZPqRnpV
|
||||
1BjpuZ+jscADWlGWvwIvfx5a4vKZf/Fj+uhqcbCU4OCHRqj2qWdWeHRlO/uDDFCm
|
||||
LahRHaBYvSpnljQhlsuYLOCDii0Fz93tvnZjhM633p6p+yFkZ5jgVZ+7jJDhqXwj
|
||||
RF2j61diRHlIIIok9KFZlrPOjtWdfKaaFMsPMQlp+vhpVa6pEBGtTtMQfuJl1puM
|
||||
rA3NtKkyCVgF8iuNrLQzyGGy2WOShhxz+U830HcYAkha6ye0fU8FXXdKFTXZd0p/
|
||||
neu8iXLKuiGvnsMe1/E92r1uFNmmwQCpCjGNstV2rpoOmHVaB9MQ5YUEEVM=
|
||||
-----END CERTIFICATE-----
|
||||
@ -1,9 +0,0 @@
|
||||
#!/bin/bash
|
||||
if [ "$1" = "" ]; then
|
||||
echo 'Exit. Your must enter hostname (CN) !'
|
||||
exit 1
|
||||
fi
|
||||
#cd /home/agm/ssl/
|
||||
HOST_NAME="$1"
|
||||
SUBJECT="/C=CA/ST=ON/L=Barrie/O=AG-NAV Inc./OU=SD/CN=$HOST_NAME/emailAddress=software@agnav.com"
|
||||
openssl req -new -nodes -newkey rsa:2048 -subj "$SUBJECT" -keyout $HOST_NAME.key -out $HOST_NAME.csr
|
||||
@ -1,13 +0,0 @@
|
||||
#!/bin/bash
|
||||
if [ "$1" = "" ]; then
|
||||
echo 'Exit. Your must enter hostname (CN) !'
|
||||
exit 1
|
||||
fi
|
||||
#cd /home/agm/ssl/
|
||||
HOST_NAME="$1"
|
||||
|
||||
# Sign the certificate with the root CA
|
||||
openssl x509 -req -days 7300 -in $HOST_NAME.csr -CA ./rootCA/rootCA.crt -CAkey ./rootCA/rootCA.key -CAcreateserial -out ./$HOST_NAME.crt -sha256 -extfile v3-ext.cnf
|
||||
|
||||
# Make pem file combining the key and certificate file
|
||||
cat $HOST_NAME.key $HOST_NAME.crt > $HOST_NAME.pem
|
||||
@ -1,9 +0,0 @@
|
||||
#!/bin/bash
|
||||
if [ "$1" = "" ]; then
|
||||
echo 'Exit. Your must enter client hostname (CN) !'
|
||||
exit 1
|
||||
fi
|
||||
#cd /home/agm/ssl/
|
||||
HOST_NAME="$1"
|
||||
SUBJECT="/C=CA/ST=ON/L=Barrie/O=AG-NAV Inc./OU=MONGO_CLIENTS/CN=$HOST_NAME/emailAddress=software@agnav.com"
|
||||
openssl req -new -nodes -newkey rsa:2048 -subj "$SUBJECT" -keyout $HOST_NAME.key -out $HOST_NAME.csr
|
||||
@ -1,13 +0,0 @@
|
||||
#!/bin/bash
|
||||
if [ "$1" = "" ]; then
|
||||
echo 'Exit. Your must enter the client hostname (CN) !'
|
||||
exit 1
|
||||
fi
|
||||
#cd /home/agm/ssl/
|
||||
HOST_NAME="$1"
|
||||
|
||||
# Sign the certificate with the root CA
|
||||
openssl x509 -req -days 7300 -in $HOST_NAME.csr -CA ./rootCA/rootCA.crt -CAkey ./rootCA/rootCA.key -CAcreateserial -out ./$HOST_NAME.crt -sha256 -extfile v3-ext_client.cnf
|
||||
|
||||
# Make pem file combining the key and certificate file
|
||||
cat $HOST_NAME.key $HOST_NAME.crt > $HOST_NAME.pem
|
||||
@ -1,30 +0,0 @@
|
||||
[req]
|
||||
default_bits = 4096
|
||||
req_extensions = extension_requirements
|
||||
distinguished_name = dn_requirements
|
||||
|
||||
[extension_requirements]
|
||||
basicConstraints = critical,CA:true
|
||||
keyUsage = critical, digitalSignature, keyEncipherment, keyCertSign, cRLSign
|
||||
subjectAltName = @sans_list
|
||||
|
||||
[dn_requirements]
|
||||
countryName = CA
|
||||
countryName_default = CA
|
||||
stateOrProvinceName = ON
|
||||
stateOrProvinceName_default = ON
|
||||
localityName = Barrie
|
||||
localityName_default = Barrie
|
||||
0.organizationName = AG-NAV Inc.
|
||||
0.organizationName_default = AG-NAV Inc.
|
||||
organizationalUnitName = SD
|
||||
organizationalUnitName_default = SD
|
||||
commonName = agnav.com
|
||||
commonName_default = agnav.com
|
||||
emailAddress = software@agnav.com
|
||||
emailAddress_default = software@agnav.com
|
||||
|
||||
[sans_list]
|
||||
DNS.1 = localhost
|
||||
DNS.2 = *.agnav.com
|
||||
IP.1 = 127.0.0.1
|
||||
@ -1,22 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDkzCCAnsCFFIW4PYNbbRGzL3YUw+vyaLleLXAMA0GCSqGSIb3DQEBCwUAMIGF
|
||||
MQswCQYDVQQGEwJDQTELMAkGA1UECAwCT04xDzANBgNVBAcMBkJhcnJpZTEUMBIG
|
||||
A1UECgwLQUctTkFWIEluYy4xCzAJBgNVBAsMAlNEMRIwEAYDVQQDDAlhZ25hdi5j
|
||||
b20xITAfBgkqhkiG9w0BCQEWEnNvZnR3YXJlQGFnbmF2LmNvbTAeFw0yNTAxMTAx
|
||||
NzE3NDBaFw00NTAxMDUxNzE3NDBaMIGFMQswCQYDVQQGEwJDQTELMAkGA1UECAwC
|
||||
T04xDzANBgNVBAcMBkJhcnJpZTEUMBIGA1UECgwLQUctTkFWIEluYy4xCzAJBgNV
|
||||
BAsMAlNEMRIwEAYDVQQDDAlhZ25hdi5jb20xITAfBgkqhkiG9w0BCQEWEnNvZnR3
|
||||
YXJlQGFnbmF2LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPO6
|
||||
zGIpSaBKYliTuGBvMWxXzrCvUUlqYAB2vJeBJf8prbfL2rRM5ac1vXx3HOFQOXF5
|
||||
dwJz+XaN45xkfYLNPta6kGTn0RDaFUpoE70SIsEP7Q1D3FhXMAocXhfSy6sZeZB/
|
||||
XKJ0dCjuVDOxjle6ksQdiKrJFzVF1xWwWijGD66jzDIcQJTCJqxyMblt/YQaI+q8
|
||||
uL2/wjiNRYALtSpd77HOQ075/Gd2sN8ppDCkpcg1F6Sx1Z9WcQx9pB/pI8wuuvdg
|
||||
xJsO1xpLsvltFMBcFTfIZYJckNPL/wEFL6XSuFBZ6S8LLTeNL8vk2JfZXai3rsXb
|
||||
LyTrFvVOavtcltcI5UECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA374T4MtxTCU/
|
||||
9VR7+aRjXb5QnTTBtigCGjo5la8eIQZiFnk8FJJZEIYI4obphTleAKAfypDlQ+Wj
|
||||
JY/4bOWvnjZIHi7XOnGLx6n9fSv4pC2PzMHThwHvY+vPGrHtoYT1xtfCRzaAMdvS
|
||||
kXbknniLS+QarurADQA5MT4v+gGuK4dq+LvgPWOhmr3/VTmdfFJQCb0J/JD8CcEu
|
||||
MSABLSIwmcu0tDd7647SkTQKBM5vdQVL15eBCMwf/sIYKokqy+It9xRzdFsEYviz
|
||||
sK2CtuheeEhJBU98ACAS2gK5CZKNTrSFNk5uTlVfUKMdF6FMmQ/vSPX+xfRpkS3R
|
||||
AWeVHLZ4OQ==
|
||||
-----END CERTIFICATE-----
|
||||
@ -1,30 +0,0 @@
|
||||
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||
MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIx3mXoPi8+oMCAggA
|
||||
MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBC846VrlknwllZXsk134KhHBIIE
|
||||
0LY2sSuhf7XLFYKHi/xZXwxviUVTe0y2aZErFSa2G+swiQvU2quz2rOiBG+t9bPW
|
||||
EEIBOBXDyUExnVcK+P7Ka64CB/ugiaAu7Tb9V+spR13wLoclhxrTGFbgAG3GHbth
|
||||
XMPbtsrlQbQtZMxUvQR+ot6RtzuQ7RUCVStDdFCBk5ZG+UwpuKtlHT0JrhquSqWs
|
||||
3kdCY3y78C2eaOnENY7dKb9T+2UL0jNRvPCOWX/uiMSibdSAmeRVZwSzsmtTLDQn
|
||||
gke0JY/qV3gBYpSbFC1xEsbYvGyKIyg2E4yNJty0kWQJtkDAR4k4VBBghh9Fg2rk
|
||||
QvNcHSTkkhyvkL8pUMUPE4h8kjOwu89nrSYQrvgpe8Y1E6om+YXdj9ilnwy0/WWi
|
||||
hsz29TXY9mnEHqFTtLPRbwCdVFhApbJPXA9CV1S1q2BVyCWSUt0kxb3/bcEqdg2n
|
||||
8SFk65CvbD3n3j7xpl2VM7WIb6rMzEG7QoNGSkt19j9km2n0zoFFfc0hkjmA39QQ
|
||||
BjPWo0hOZu8d0nqg3wajnM6Qu6cB/HmhZubILNZ5cpbbm7OrhpV2wG/4TmfbBRSn
|
||||
f98m0sdPAMhJ5cpvgC0b5GvxD+8QGBAG9nkvHWulMtH9do2x3yfLECPj7S65O0Sd
|
||||
liVH6sJNeiC2lCBT+QEBBpbxkL1INZ2mPaCJ4wC4/oufTC1c9/Qd7nJCKiTtbdYb
|
||||
ngcV6xKH0o04mFhZSZQECvWn6tdZ8Syq6rdYjB+Ab11qGYmXuUptq5wawUZ4o6yd
|
||||
q/VucdpYgkYLZ7UQ2yQiUr3tUKEPJyk2/EgMm7YZdelUTdj5GcUVmOkDMdDXCZGq
|
||||
o188f3NssNY8n+g37/TZUfxRC7jGyS8EUfXWFD+MJrIVJq2uGqDAvDtZ+qqiJaq7
|
||||
2NLeJzhN1SW9Eay9idKHp21GIh5rG7V+AXzcMW0N0ATbYohFSYSxcYayPoGKbJN1
|
||||
9TWEFnJE1htWWhKM1XWfan1H9ucV22eMwGMdV+MV0+QukOFnIqjJqGIHT9+mKK6w
|
||||
5daAyg8HOjP9/xLmRjkafY2sAKi7QmvjbrfFJYNAIL59jhu5AsI7RFJtjhfJ/ye+
|
||||
n5tvJjHs1YqOjG1+Q+z2EtGHsWBD6dk9FuEsKCly2/C0g0DXQOVm01kez4OFbSUN
|
||||
nfOBSJwvUIEJm5X9YAWzNlFbIgFNq7plC95eUq78Sn7kLNN2yUaVBKxVbnEQKYqd
|
||||
Lg60j2ZuyK5x9gdmAViX4Gp84rXHKN1w8oJFPcaoT9Tn91n6UaxDMTQ9IrACxXnI
|
||||
MBrbwfE3yZGyP7IpG4Ugnn0l38GO0alqfTKf98G/xbg+voe7udSFdCw3WElt8OvK
|
||||
KQLe4SpfItjBErs+q4l/4P7NKc/vHoGmompc3cDtu8W8Xx8hE+bLJczMm01KvYeN
|
||||
4Ev7x4SuPnweDZn7pVF3czFfoIKJZSBFiXCaQdymKqcgJJWOUzvzY5ZFkzv1CB3S
|
||||
/AmmJyLAWp0tWbFOPyj4B+maKJwFcP3aNl0HSx1JoAQOmbt5WQ3+LNIkt519SmZc
|
||||
U4qGnQy1NXtgL8DNpIIiHIyBkYHT6U2lQCB+Xri6TWC0HPLOMtxzwpqcpMAxlaL/
|
||||
9tj4emEgSjEe4M3OMtA9MMjUme5BUMXUgYfidjWsKAd2
|
||||
-----END ENCRYPTED PRIVATE KEY-----
|
||||
@ -1 +0,0 @@
|
||||
72437C49EFD12BF6AC91F89353004FCF03737A13
|
||||
@ -1,11 +0,0 @@
|
||||
authorityKeyIdentifier=keyid,issuer
|
||||
basicConstraints=CA:FALSE
|
||||
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
|
||||
#extendedKeyUsage = clientAuth, serverAuth
|
||||
subjectAltName = @alt_names
|
||||
|
||||
[alt_names]
|
||||
#DNS.1 = localhost
|
||||
#IP.1 = 127.0.0.1
|
||||
# For client/replica members of agnav.com
|
||||
DNS.1 = *.agnav.com
|
||||
@ -1,13 +0,0 @@
|
||||
authorityKeyIdentifier=keyid,issuer
|
||||
basicConstraints=CA:FALSE
|
||||
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
|
||||
#extendedKeyUsage = clientAuth, serverAuth
|
||||
subjectAltName = @alt_names
|
||||
|
||||
[alt_names]
|
||||
# For localhost
|
||||
DNS.1 = localhost
|
||||
IP.1 = 127.0.0.1
|
||||
|
||||
# For client of agnav
|
||||
#DNS.1 = *.agnav.com
|
||||
@ -1,6 +0,0 @@
|
||||
# Backup schedule in AgMission server to AGNAS within AgNav
|
||||
# m h dom mon dow command
|
||||
# Run backup of agmission db every 3 days at 21:00 midnight
|
||||
0 21 */3 * * /home/agmission/backups/scripts/backup_agm.sh
|
||||
# Run system backup every 1st day of the month at 1:30 am
|
||||
30 1 1 * * /home/agmission/backups/scripts/backup_sys.sh
|
||||
@ -1,34 +0,0 @@
|
||||
{
|
||||
"apps": [
|
||||
{
|
||||
"name": "agmission-prod",
|
||||
"version": "3.0.0",
|
||||
"script": "server.js",
|
||||
"node_args": [
|
||||
"--expose-gc",
|
||||
"--max-old-space-size=2048",
|
||||
"--nouse-idle-notification",
|
||||
"--icu-data-dir=/home/agmission/.nvm/versions/node/v16.20.2/lib/node_modules/full-icu"
|
||||
],
|
||||
"watch": false,
|
||||
"ignore_watch": [
|
||||
"[\/\\]\\./",
|
||||
"node_modules",
|
||||
".tmp",
|
||||
"job-unzip",
|
||||
"job-uploads",
|
||||
"backup"
|
||||
],
|
||||
"merge_logs": true,
|
||||
"cwd": "/media/ssd1/agmission",
|
||||
"env": {
|
||||
"DISPLAY": ":99",
|
||||
"NODE_ENV": "production",
|
||||
"AGM_PORT": "7000",
|
||||
"PRODUCTION": 1,
|
||||
"DEBUG": "agm:*"
|
||||
},
|
||||
"log_date_format": "YYYY-MM-DD HH:mm:ss"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Variables
|
||||
agnroot='/media/ssd1/agmission'
|
||||
bkroot='/home/agmission/backups'
|
||||
fname=agmdb_$(date +\%Y\%m\%d)
|
||||
logfn=$bkroot/logs/agm_$(date +\%Y\%m\%d).log
|
||||
uplogfn=$bkroot/logs/agm_files_$(date +\%Y\%m\%d).log
|
||||
|
||||
# Dump db to file then gzip
|
||||
mongodump --archive=$bkroot/$fname.gz --gzip --db agmission --username "agm" --password "Agm2017" --authenticationDatabase "agmission" --forceTableScan
|
||||
|
||||
find $bkroot/agmdb_*.gz -mtime +11 -exec rm {} \;
|
||||
find $bkroot/logs/agm_*.log $bkroot/logs/agm_files_*.log -mtime +11 -exec rm {} \;
|
||||
|
||||
# Sync agm db to NAS
|
||||
rsync -arzh --delete --exclude '/uploads' --exclude '/sys' --exclude '/scripts' --exclude '/logs' $bkroot/ rsync://rsync@data.agnav.com:/agm/ --password-file $bkroot/scripts/pass --log-file $logfn --stats --ignore-existing
|
||||
|
||||
# Sync job uploaded files to NAS
|
||||
rsync -arzh --delete-delay $agnroot/job-uploads/ rsync://rsync@data.agnav.com:/agm/uploads/ --password-file $bkroot/scripts/pass --log-file $uplogfn --stats --ignore-existing
|
||||
|
||||
# Sync the rsync log files to the destination
|
||||
rsync -arzh --delete-delay $bkroot/logs/ rsync://rsync@data.agnav.com:/agm/logs/ --password-file $bkroot/scripts/pass
|
||||
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
#!/bin/sh
|
||||
bkroot='/home/agmission/backups'
|
||||
fname=$bkroot/sys/sys_$(date +\%Y\%m\%d).tgz
|
||||
logfn=$bkroot/logs/agm_sys_$(date +\%Y\%m\%d).log
|
||||
|
||||
find $bkroot/sys/sys_*.tgz -mtime +32 -exec rm {} \;
|
||||
|
||||
sudo tar -cpzf $fname --exclude=$bkroot --exclude='/home/agmission' --exclude='/media/ssd1' / --one-file-system
|
||||
|
||||
# Sync job uploaded files to NAS
|
||||
rsync -arzhP --delete-delay $bkroot/sys/ rsync://rsync@data.agnav.com:/agm/sys/ --password-file $bkroot/scripts/pass --log-file $logfn --stats --ignore-existing
|
||||
|
||||
# Sync the rsync log files to the destination
|
||||
rsync -arzh --delete-delay $bkroot/logs/ rsync://rsync@data.agnav.com:/agm/logs/ --password-file $bkroot/scripts/pass
|
||||
@ -1,2 +0,0 @@
|
||||
#!/bin/sh
|
||||
sudo tar -cpzf /home/trung/bk_$(date +\%Y\%m\%d).tgz /home/trung/temp
|
||||
@ -1,51 +0,0 @@
|
||||
// Aggregate from user to user's settings
|
||||
var custUsers = db.customers.aggregate([
|
||||
{ $match: { active: true } },
|
||||
// { $project: { lang: { $ifNull: [ "$lang", "en" ] } } },
|
||||
{
|
||||
$lookup:
|
||||
{
|
||||
from: "users",
|
||||
localField: "user",
|
||||
foreignField: "_id",
|
||||
as: "c_users"
|
||||
}
|
||||
},
|
||||
{ $unwind: { path: "$c_users", "preserveNullAndEmptyArrays": true } },
|
||||
{
|
||||
$project: {
|
||||
'_id': 0,
|
||||
'id': '$c_users._id',
|
||||
'name': 1,
|
||||
// 'contact': 1,
|
||||
'username':'$c_users.username'
|
||||
}
|
||||
}
|
||||
]).toArray();
|
||||
// print (custUsers)
|
||||
var phead = true, hline = '', line = '';
|
||||
custUsers.forEach(cu => {
|
||||
for (var p in cu) {
|
||||
|
||||
if (phead) {
|
||||
if (p == 'id')
|
||||
hline = 'userId,' + hline;
|
||||
else
|
||||
hline += p + ','
|
||||
}
|
||||
if (p == 'id')
|
||||
line = cu[p] + ',' + line;
|
||||
else {
|
||||
// if (cu[p].indexOf(',') != -1)
|
||||
// print (cu[p])
|
||||
cu[p] = (cu[p] || '').replace(/,/g, '');
|
||||
line = line + cu[p] + ',';
|
||||
}
|
||||
}
|
||||
if (phead) {
|
||||
print (hline.slice(0, -1))
|
||||
phead = false;
|
||||
}
|
||||
print (line.slice(0, -1))
|
||||
line = '';
|
||||
});
|
||||
@ -1,25 +0,0 @@
|
||||
{
|
||||
"apps": [
|
||||
{
|
||||
"interpreter" : "node@14.17.2",
|
||||
"name": "cleanup-worker",
|
||||
"script": "cleanup_worker.js",
|
||||
"node_args" : ["--expose-gc", "--max-old-space-size=1048", "--nouse-idle-notification"],
|
||||
"watch": false,
|
||||
"exec_mode": "fork",
|
||||
"instances": 1,
|
||||
"cwd": "/media/ssd1/agmission/workers",
|
||||
"error_file": "~/.pm2/logs/err.log",
|
||||
"out_file": "~/.pm2/logs/out.log",
|
||||
"merge_logs": false,
|
||||
"env": {
|
||||
"NODE_ENV": "production",
|
||||
"PRODUCTION": 1,
|
||||
"DEBUG": "agm:*"
|
||||
},
|
||||
"max_restarts" : 10,
|
||||
"min_uptime" : "24h",
|
||||
"log_date_format": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,67 +0,0 @@
|
||||
Full deployment documentation: trunk/Documents/DEPLOYMENT.md
|
||||
|
||||
Configuration Variables:
|
||||
AGM_BASE_DIR - Local AgMission directory
|
||||
AGN_LIBS_DIR - Local AGN libraries directory
|
||||
AGM_DEST_HOST - Remote server (user@hostname)
|
||||
AGM_DEST_PORT - SSH port
|
||||
AGM_DEST_PATH - Remote destination directory
|
||||
|
||||
Command Backend Frontend Use Case
|
||||
.[agm-deploy.sh] 0 branch Dry-run Dry-run Test everything safely
|
||||
.[agm-deploy.sh] 1 branch Deploy Skipped Backend-only deployment
|
||||
.[agm-deploy.sh] 2 branch dry-run Deploy Dry-run Deploy backend, test frontend
|
||||
.[agm-deploy.sh] 2 branch Deploy Deploy Full production deployment
|
||||
|
||||
Usage Examples:
|
||||
One-time override:
|
||||
# Custom base directory
|
||||
AGM_BASE_DIR="/projects/AgMission" ./agm-deploy.sh 1 trunk
|
||||
|
||||
# Different AGN libs location
|
||||
AGN_LIBS_DIR="/opt/@agn" ./agm-deploy.sh 2 subscription-signup
|
||||
|
||||
# Deploy to staging server
|
||||
AGM_DEST_HOST="agm@staging.agnav.com" ./agm-deploy.sh 1 main
|
||||
|
||||
Using configuration file:
|
||||
# Create config file
|
||||
cp agm-deploy.conf.template ~/.agm-deploy.conf
|
||||
nano ~/.agm-deploy.conf
|
||||
|
||||
# Use the config
|
||||
source ~/.agm-deploy.conf && ./agm-deploy.sh 1 trunk
|
||||
|
||||
Environment-specific configs:
|
||||
# Development
|
||||
source ~/.agm-deploy-dev.conf && ./agm-deploy.sh 2 subscription-signup
|
||||
|
||||
# Staging
|
||||
source ~/.agm-deploy-staging.conf && ./agm-deploy.sh 1 main
|
||||
|
||||
# Production
|
||||
source ~/.agm-deploy-prod.conf && ./agm-deploy.sh 1 trunk
|
||||
|
||||
Benefits:
|
||||
✅ Flexible development environments - Different developers can use different paths
|
||||
✅ Multi-environment support - Easy switching between dev/staging/prod
|
||||
✅ CI/CD friendly - Environment variables work well in automation
|
||||
✅ Backwards compatible - Still works with default paths if no overrides
|
||||
✅ Clear feedback - Shows exactly which paths are being used
|
||||
✅ Template provided - Easy to get started with custom configurations
|
||||
|
||||
Advanced Usage:
|
||||
# Deploy from custom location to staging
|
||||
AGM_BASE_DIR="/backup/AgMission" \
|
||||
AGM_DEST_HOST="agm@staging.com" \
|
||||
AGM_DEST_PORT="2222" \
|
||||
./agm-deploy.sh 2 subscription-signup
|
||||
|
||||
# Use alternative AGN libs
|
||||
AGN_LIBS_DIR="/shared/@agn" ./agm-deploy.sh 1 trunk
|
||||
|
||||
Summary:
|
||||
The script is now much more flexible and can adapt to different development environments and deployment targets without code changes!
|
||||
This makes the deployment script completely environment-agnostic and suitable for any deployment scenario!
|
||||
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
# AGM Deployment Configuration Template
|
||||
# Copy this file to ~/.agm-deploy.conf and customize as needed
|
||||
|
||||
# Base directories
|
||||
export AGM_BASE_DIR="$HOME/work/AgMission"
|
||||
export AGN_LIBS_DIR="$HOME/work/@agn"
|
||||
|
||||
# Deployment targets
|
||||
export AGM_DEST_HOST="agm@agmission-1.agnav.com"
|
||||
export AGM_DEST_PORT="22222"
|
||||
export AGM_DEST_PATH="/home/agm/apps"
|
||||
|
||||
# Alternative configurations (uncomment and modify as needed)
|
||||
|
||||
# For development environment:
|
||||
# export AGM_DEST_HOST="agm@dev.agnav.com"
|
||||
# export AGM_DEST_PORT="2222"
|
||||
# export AGM_DEST_PATH="/home/dev/applications"
|
||||
|
||||
# For staging environment:
|
||||
# export AGM_DEST_HOST="agm@staging.agnav.com"
|
||||
# export AGM_DEST_PORT="22222"
|
||||
# export AGM_DEST_PATH="/opt/staging/apps"
|
||||
|
||||
# For custom local paths:
|
||||
# export AGM_BASE_DIR="/custom/path/to/AgMission"
|
||||
# export AGN_LIBS_DIR="/custom/path/to/@agn"
|
||||
# export AGM_DEST_PATH="/custom/remote/path"
|
||||
|
||||
# Usage:
|
||||
# 1. Copy this file: cp agm-deploy.conf.template ~/.agm-deploy.conf
|
||||
# 2. Edit the configuration: nano ~/.agm-deploy.conf
|
||||
# 3. Source it before deployment: source ~/.agm-deploy.conf && ./agm-deploy.sh 1 trunk
|
||||
# 4. Or create environment-specific configs:
|
||||
# - ~/.agm-deploy-dev.conf
|
||||
# - ~/.agm-deploy-staging.conf
|
||||
# - ~/.agm-deploy-prod.conf
|
||||
@ -1,300 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# AGM Deployment Script
|
||||
# Usage: ./agm-deploy.sh [run_mode] [branch_name] [fe_mode]
|
||||
# run_mode: 0=dry-run, 1=deploy-backend, 2=deploy-all
|
||||
# branch_name: branch to deploy from (default: subscription-invoicing)
|
||||
# fe_mode: frontend mode for mode 2 (run|dry-run), optional
|
||||
#
|
||||
# Environment Variables (optional overrides):
|
||||
# AGM_BASE_DIR - Base AgMission directory (default: ~/work/AgMission)
|
||||
# AGN_LIBS_DIR - AGN libraries directory (default: ~/work/@agn)
|
||||
# AGM_DEST_HOST - Deployment target host (default: agm@agmission-1.agnav.com)
|
||||
# AGM_DEST_PORT - SSH port (default: 22222)
|
||||
# AGM_DEST_PATH - Remote destination path (default: /home/agm/apps)
|
||||
#
|
||||
# Examples:
|
||||
# ./agm-deploy.sh # Dry run with default branch
|
||||
# ./agm-deploy.sh 1 # Deploy backend from default branch
|
||||
# ./agm-deploy.sh 1 subscription-signup # Deploy backend from subscription-signup branch
|
||||
# ./agm-deploy.sh 2 subscription-signup # Deploy everything from subscription-signup branch
|
||||
# ./agm-deploy.sh 2 subscription-signup dry-run # Deploy backend, dry-run frontend
|
||||
# AGM_BASE_DIR=/custom/path ./agm-deploy.sh 1 # Use custom base directory
|
||||
# AGM_DEST_PATH=/opt/apps ./agm-deploy.sh 2 trunk # Deploy to custom remote path
|
||||
|
||||
# Show help if requested
|
||||
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
|
||||
echo "AGM Deployment Script"
|
||||
echo ""
|
||||
echo "Usage: $0 [run_mode] [branch_name] [fe_mode]"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " run_mode - Deployment mode:"
|
||||
echo " 0 or empty = dry-run (show what would be deployed)"
|
||||
echo " 1 = deploy backend only"
|
||||
echo " 2 = deploy backend and frontend"
|
||||
echo " branch_name - Branch to deploy from (default: subscription-invoicing)"
|
||||
echo " Use 'trunk' or 'main' to deploy from SVN trunk"
|
||||
echo " fe_mode - Frontend mode for mode 2 (run|dry-run), optional"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 # Dry run with default branch"
|
||||
echo " $0 1 # Deploy backend from default branch"
|
||||
echo " $0 1 subscription-signup # Deploy backend from subscription-signup"
|
||||
echo " $0 2 subscription-signup # Deploy everything from subscription-signup"
|
||||
echo " $0 2 subscription-signup dry-run # Deploy backend, dry-run frontend"
|
||||
echo " $0 1 trunk # Deploy backend from SVN trunk"
|
||||
echo " $0 2 main # Deploy everything from SVN trunk (alias)"
|
||||
echo ""
|
||||
echo "Available branches:"
|
||||
# Use configurable base directory if set, otherwise default
|
||||
BRANCH_LIST_DIR="${AGM_BASE_DIR:-~/work/AgMission}/branches"
|
||||
BRANCH_LIST_DIR=$(eval echo "$BRANCH_LIST_DIR")
|
||||
ls "$BRANCH_LIST_DIR/" 2>/dev/null || echo "No branches directory found"
|
||||
echo ""
|
||||
echo "SVN Trunk location: ${AGM_BASE_DIR:-~/work/AgMission}/trunk/Development"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Validate run mode parameter
|
||||
if [ -n "$1" ]; then
|
||||
case "$1" in
|
||||
0|1|2)
|
||||
# Valid parameters
|
||||
;;
|
||||
*)
|
||||
echo "Error: Invalid run mode '$1'"
|
||||
echo ""
|
||||
echo "Supported run modes:"
|
||||
echo " 0 or empty = dry-run (show what would be deployed)"
|
||||
echo " 1 = deploy backend only"
|
||||
echo " 2 = deploy backend and frontend"
|
||||
echo ""
|
||||
echo "Use '$0 --help' for more information"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Configurable paths - can be overridden by environment variables
|
||||
AGM_BASE_DIR="${AGM_BASE_DIR:-~/work/AgMission}"
|
||||
AGN_LIBS_DIR="${AGN_LIBS_DIR:-~/work/@agn}"
|
||||
AGM_DEST_HOST="${AGM_DEST_HOST:-agm@agmission-1.agnav.com}"
|
||||
AGM_DEST_PORT="${AGM_DEST_PORT:-22222}"
|
||||
AGM_DEST_PATH="${AGM_DEST_PATH:-/home/agm/apps}"
|
||||
|
||||
# Expand tilde in paths
|
||||
AGM_BASE_DIR=$(eval echo "$AGM_BASE_DIR")
|
||||
AGN_LIBS_DIR=$(eval echo "$AGN_LIBS_DIR")
|
||||
|
||||
# Display configuration
|
||||
echo "=== Path Configuration ==="
|
||||
echo "AGM Base Directory: $AGM_BASE_DIR"
|
||||
echo "AGN Libraries Directory: $AGN_LIBS_DIR"
|
||||
echo "Deployment Target: $AGM_DEST_HOST:$AGM_DEST_PORT"
|
||||
echo "Remote Destination Path: $AGM_DEST_PATH"
|
||||
echo "=========================="
|
||||
|
||||
# Configuration - Easy to modify deployment sources
|
||||
DEFAULT_BRANCH="subscription-invoicing"
|
||||
BRANCH="${2:-$DEFAULT_BRANCH}" # Use second argument or default branch
|
||||
|
||||
# Determine source path based on branch name
|
||||
if [ "$BRANCH" = "trunk" ] || [ "$BRANCH" = "main" ]; then
|
||||
SOURCE_ROOT="$AGM_BASE_DIR/trunk/Development"
|
||||
BRANCH_TYPE="trunk"
|
||||
DISPLAY_NAME="trunk (main)"
|
||||
else
|
||||
SOURCE_ROOT="$AGM_BASE_DIR/branches/$BRANCH"
|
||||
BRANCH_TYPE="branch"
|
||||
DISPLAY_NAME="branch: $BRANCH"
|
||||
fi
|
||||
|
||||
# Validate that the source directory exists
|
||||
if [ ! -d "$SOURCE_ROOT" ]; then
|
||||
echo "Error: Source directory $SOURCE_ROOT does not exist"
|
||||
echo ""
|
||||
if [ "$BRANCH_TYPE" = "trunk" ]; then
|
||||
echo "Trunk directory not found. Expected: $AGM_BASE_DIR/trunk/Development"
|
||||
else
|
||||
echo "Available branches:"
|
||||
ls "$AGM_BASE_DIR/branches/" 2>/dev/null || echo "No branches directory found"
|
||||
echo ""
|
||||
echo "To deploy from trunk/main, use: $0 [mode] trunk"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Display current configuration
|
||||
echo "=== Deployment Configuration ==="
|
||||
echo "Source Type: $DISPLAY_NAME"
|
||||
echo "Source Path: $SOURCE_ROOT"
|
||||
echo "================================"
|
||||
|
||||
# Use configurable AGN libraries path
|
||||
agnLibs="$AGN_LIBS_DIR/"
|
||||
|
||||
# Set paths based on source type
|
||||
if [ "$BRANCH_TYPE" = "trunk" ]; then
|
||||
# Trunk structure (Development directory)
|
||||
agmFE="$SOURCE_ROOT/client/dist/"
|
||||
agmBE="$SOURCE_ROOT/server/"
|
||||
else
|
||||
# Branch structure
|
||||
agmFE="$SOURCE_ROOT/client/dist/"
|
||||
agmBE="$SOURCE_ROOT/server/"
|
||||
fi
|
||||
|
||||
# Validate that required directories exist
|
||||
if [ ! -d "$agmFE" ]; then
|
||||
echo "Warning: Frontend dist directory not found: $agmFE"
|
||||
echo "You may need to build the frontend first: npm run build"
|
||||
fi
|
||||
|
||||
if [ ! -d "$agmBE" ]; then
|
||||
echo "Error: Backend directory not found: $agmBE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Use configurable base directory for track and GPS servers
|
||||
trackSrv="$AGM_BASE_DIR/trunk/Development/track-server/"
|
||||
gpsSrv="$AGM_BASE_DIR/trunk/Development/gps-server/"
|
||||
sharedMods="$AGM_BASE_DIR/trunk/Development/shared/"
|
||||
|
||||
# Use configurable destination settings
|
||||
destUserHost="$AGM_DEST_HOST"
|
||||
destPort="$AGM_DEST_PORT"
|
||||
destRoot="$destUserHost:$AGM_DEST_PATH"
|
||||
|
||||
curDate=$(date +\%Y\%m\%d)
|
||||
DIR="$(cd "$(dirname "$0")" && pwd)" # Get the current script directory
|
||||
echo "PWD: ${DIR}"
|
||||
|
||||
# Create logs directory if not exists
|
||||
logdir=$DIR/logs
|
||||
ls $logdir &>/dev/null || mkdir $logdir
|
||||
|
||||
# Default options for rsync, --size-only
|
||||
rsync_args=(-ahiz --stats --exclude-from='excludes.txt')
|
||||
sshCmd="ssh -p $destPort"
|
||||
|
||||
# Determine run mode and frontend deployment flag
|
||||
RUN_MODE="dry-run" # Default to safe mode
|
||||
DEPLOY_FRONTEND=false
|
||||
FE_DRY_RUN=false
|
||||
|
||||
# Normalize fe_mode: strip leading dashes so both "dry-run" and "--dry-run" work
|
||||
FE_MODE_ARG="${3#--}"
|
||||
|
||||
case "${1:-0}" in
|
||||
""|"0")
|
||||
RUN_MODE="dry-run"
|
||||
rsync_args+=(--dry-run)
|
||||
DEPLOY_FRONTEND=true # Show what would be deployed for frontend too
|
||||
;;
|
||||
"1")
|
||||
RUN_MODE="run"
|
||||
DEPLOY_FRONTEND=false
|
||||
;;
|
||||
"2")
|
||||
RUN_MODE="run"
|
||||
DEPLOY_FRONTEND=true
|
||||
# Check for frontend mode override (3rd parameter)
|
||||
if [ "$FE_MODE_ARG" = "dry-run" ]; then
|
||||
FE_DRY_RUN=true
|
||||
# Also apply dry-run to backend when fe_mode is dry-run
|
||||
RUN_MODE="dry-run"
|
||||
rsync_args+=(--dry-run)
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
# Make symlinks for assets of other languages, es and pt. etc.
|
||||
if [ ! -L $agmFE/es/assets ]; then
|
||||
echo "create assets link to ../assets/ in $agmFE/es/"
|
||||
cd $agmFE/es/
|
||||
rm -R assets
|
||||
ln -s ../assets/ assets
|
||||
fi
|
||||
if [ ! -L $agmFE/pt/assets ]; then
|
||||
echo "create assets link to ../assets/ in $agmFE/pt/"
|
||||
cd $agmFE/pt/
|
||||
rm -R assets
|
||||
ln -s ../assets/ assets
|
||||
fi
|
||||
|
||||
# CD back to the current script directory
|
||||
cd $DIR
|
||||
#echo "$( cd "$( dirname "$0" )" && pwd )"
|
||||
|
||||
echo "Run mode: $RUN_MODE"
|
||||
|
||||
# Sync the base libs under /@agn
|
||||
echo "====== For @agn libs, run mode: $RUN_MODE"
|
||||
if [ "$RUN_MODE" != "dry-run" ]; then
|
||||
logfile=$logdir/@agn_$curDate.log
|
||||
rsync_ops="${rsync_args[@]} --log-file=$logfile"
|
||||
else
|
||||
rsync_ops="${rsync_args[@]}"
|
||||
fi
|
||||
rsync $rsync_ops $agnLibs $destRoot/@agn/ -e "${sshCmd}"
|
||||
|
||||
# Sync Agmission BE
|
||||
echo "====== For Agmission BE, run mode: $RUN_MODE"
|
||||
if [ "$RUN_MODE" != "dry-run" ]; then
|
||||
logfile=$logdir/agmBE_$curDate.log
|
||||
rsync_ops="${rsync_args[@]} --log-file=$logfile"
|
||||
else
|
||||
rsync_ops="${rsync_args[@]}"
|
||||
fi
|
||||
rsync $rsync_ops $agmBE $destRoot/agmission/ -e "${sshCmd}"
|
||||
|
||||
# Sync Agmission FE
|
||||
if [ "$DEPLOY_FRONTEND" = true ]; then
|
||||
echo "====== For Agmission FE, run mode: $RUN_MODE"
|
||||
if [ "$FE_DRY_RUN" = true ]; then
|
||||
fe_rsync_ops="${rsync_args[@]} --dry-run"
|
||||
echo "FE dry-run (frontend-specific dry-run mode)"
|
||||
elif [ "$RUN_MODE" = "run" ]; then
|
||||
logfile=$logdir/agmFE_$curDate.log
|
||||
fe_rsync_ops="${rsync_args[@]} --log-file=$logfile"
|
||||
echo "FE deployment enabled"
|
||||
else
|
||||
fe_rsync_ops="${rsync_args[@]} --dry-run"
|
||||
echo "FE dry-run (global dry-run mode)"
|
||||
fi
|
||||
rsync $fe_rsync_ops -l $agmFE $destRoot/agmission/dist-$curDate/ -e "${sshCmd}"
|
||||
else
|
||||
echo "====== For Agmission FE: SKIPPED (frontend deployment disabled)"
|
||||
fi
|
||||
## -l to copy symlinks as symlinks
|
||||
|
||||
# Sync Track Server
|
||||
echo "====== For Track Server, run mode: $RUN_MODE"
|
||||
if [ "$RUN_MODE" != "dry-run" ]; then
|
||||
logfile=$logdir/trackSrv_$curDate.log
|
||||
rsync_ops="${rsync_args[@]} --log-file=$logfile"
|
||||
else
|
||||
rsync_ops="${rsync_args[@]}"
|
||||
fi
|
||||
rsync $rsync_ops $trackSrv $destRoot/track-server/ -e "${sshCmd}"
|
||||
|
||||
# Sync GPS Server
|
||||
echo "====== For GPS Server, run mode: $RUN_MODE"
|
||||
if [ "$RUN_MODE" != "dry-run" ]; then
|
||||
logfile=$logdir/gpsSrv_$curDate.log
|
||||
rsync_ops="${rsync_args[@]} --log-file=$logfile"
|
||||
else
|
||||
rsync_ops="${rsync_args[@]}"
|
||||
fi
|
||||
rsync $rsync_ops $gpsSrv $destRoot/gps-server/ -e "${sshCmd}"
|
||||
|
||||
# Sync Others such as maintainer
|
||||
|
||||
# Run a script on the remote server to update the symlinks, node_modules and restart the pm2 apps
|
||||
# ssh -p $destPort $destUserHost "cd /home/agm/apps/agmission && ./deploy.sh $curDate"
|
||||
|
||||
# Clean up logs older than 15 days
|
||||
if [ "$RUN_MODE" != "dry-run" ]; then
|
||||
find "$logdir" -name "*.log" -mtime +15 -exec rm {} \; # | xargs -r rm {} \;
|
||||
fi
|
||||
@ -1,49 +0,0 @@
|
||||
.vscodeignore
|
||||
**/node_modules/
|
||||
**/.svn
|
||||
**/apidoc/
|
||||
apidoc.json
|
||||
reports/
|
||||
**/docs*
|
||||
**/*md
|
||||
test*
|
||||
**/.tmp
|
||||
**/demo/
|
||||
.specstory/
|
||||
# NOTES: Patterns in excludes.txt must be relative to the source directory. E.g: Use package*.json if syncing starts from the /server directory.
|
||||
package*.json
|
||||
# **/track-server/package*.json
|
||||
# **/gps-server/package*.json
|
||||
# **/error-handler/package-lock.json
|
||||
# **/mailer/package-lock.json
|
||||
.eslintrc.json
|
||||
.vscode/
|
||||
.github/
|
||||
environment*.env
|
||||
**/emails/ref-template/
|
||||
downloadMap.*.html
|
||||
**/report-01*
|
||||
**/sample-job*
|
||||
README.md
|
||||
**/*.*log
|
||||
**/workers/custList*.json
|
||||
**/scripts/**.json
|
||||
migrateToSM.js
|
||||
track_*.json
|
||||
**/satloc/*
|
||||
*.csv
|
||||
*.txt
|
||||
*.pdf
|
||||
*.log
|
||||
cleanup_*.js
|
||||
debug*.js
|
||||
*_test.js
|
||||
csv_extract.js
|
||||
db-scripts.js
|
||||
satloc_usage_*.js
|
||||
setup_*
|
||||
start_*.js
|
||||
setup_partners.js
|
||||
_examples.js
|
||||
*-pm2.json
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
{
|
||||
"apps": [
|
||||
{
|
||||
"name": "gps-server-agnav-6080",
|
||||
"script": "gps-server.js",
|
||||
"args": [
|
||||
"--max-old-space-size 1536"
|
||||
],
|
||||
"watch": false,
|
||||
"node_args": [],
|
||||
"merge_logs": false,
|
||||
"cwd": "/media/ssd1/agmission/gps-server/",
|
||||
"env": {
|
||||
"NODE_ENV": "development",
|
||||
"DEBUG": "gps-*",
|
||||
"PROTOCOL": "AGNAV",
|
||||
"LOG_DEBUG": "0",
|
||||
"LOG_RAW": "0",
|
||||
"LOG_IDS": "0000000001,0000000003, 0000000005"
|
||||
},
|
||||
"log_date_format": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
{
|
||||
"apps": [
|
||||
{
|
||||
"name": "gps-server-RAP-6082",
|
||||
"script": "gps-server.js",
|
||||
"args": [
|
||||
"--max-old-space-size 1536"
|
||||
],
|
||||
"watch": false,
|
||||
"node_args": [],
|
||||
"merge_logs": false,
|
||||
"cwd": "/media/ssd1/agmission/gps-server/",
|
||||
"env": {
|
||||
"NODE_ENV": "production",
|
||||
"DEBUG": "gps-*",
|
||||
"PROTOCOL": "RAP",
|
||||
"LOG_DEBUG": "0",
|
||||
"LOG_RAW": "0",
|
||||
"LOG_IDS": "0000000001,0000000003"
|
||||
},
|
||||
"log_date_format": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
Resolved w/ following done:
|
||||
|
||||
Stopped db brute-force and DoS attacks
|
||||
Hardened server security with configs ref: https://www.cyberciti.biz/tips/linux-security.html
|
||||
Prevented brute-force ssh password-guessing attack (ufw rules, change default ssh port, use iptables chain from rutgers university scripts, etc.)
|
||||
Tighten UFW firewall rules (allow only traffics for trusted agnav WAN IP, updated via dynamic DNS using a customer script)
|
||||
Notes: A good thing is with the auto update UFW script to periodical detect when AgNav WAN IP changed and updating related UFW firewall rule, we still do not have to add fixed static public IP to our service yet.
|
||||
|
||||
Other references:
|
||||
|
||||
https://unix.stackexchange.com/questions/91701/ufw-allow-traffic-only-from-a-domain-with-dynamic-ip-address
|
||||
https://report.cs.rutgers.edu/mrtg/drop/dropstat.cgi?start=-1week
|
||||
|
||||
Added crontab tasks (root):
|
||||
# Check and update firewall rule to allow any traffic from AgNav
|
||||
*/5 * * * * /usr/local/sbin/update-agn-UFW.sh > /usr/local/sbin/update-agn-UFW.log
|
||||
|
||||
# Check and update the iptables block chain (adds LCSRDrop) to prevent Bruteforce attackers, database from IPs registered at blocklist.de
|
||||
1-56/15 * * * * /usr/local/sbin/lcsrdrop.sh > /dev/null 2>&1
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
{
|
||||
"apps": [
|
||||
{
|
||||
"interpreter" : "node@14.17.2",
|
||||
"name": "job-importer",
|
||||
"script": "job_worker.js",
|
||||
"node_args" : ["--expose-gc", "--max-old-space-size=1048", "--nouse-idle-notification"],
|
||||
"watch": false,
|
||||
"exec_mode": "fork",
|
||||
"instances": 4,
|
||||
"cwd": "/media/ssd1/agmission/workers",
|
||||
"error_file": "~/.pm2/logs/err.log",
|
||||
"out_file": "~/.pm2/logs/out.log",
|
||||
"merge_logs": false,
|
||||
"env": {
|
||||
"NODE_ENV": "production",
|
||||
"PRODUCTION": 1,
|
||||
"DEBUG": "agm:*"
|
||||
},
|
||||
"max_restarts" : 5,
|
||||
"min_uptime" : "24h",
|
||||
"log_date_format": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
{
|
||||
"apps": [
|
||||
{
|
||||
"name": "jsreport",
|
||||
"script": "server.js",
|
||||
"node_args" : ["--max-old-space-size=1048", "--nouse-idle-notification"],
|
||||
"watch": false,
|
||||
"exec_mode": "fork",
|
||||
"instances": 1,
|
||||
"cwd": "/media/ssd1/jsreport",
|
||||
"merge_logs": true,
|
||||
"env": {
|
||||
"NODE_ENV": "production"
|
||||
},
|
||||
"log_date_format": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,456 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
#originally spamhaus drop script.
|
||||
#modified by Hanz Makmur 2015-03-13
|
||||
#using LCSR DROP file add to iptables
|
||||
# locking added by daw; 9/14/16
|
||||
# logger added by daw; 10/2/17
|
||||
# efficiency improvement: instead of flushing and re-adding entire list,
|
||||
# only delete expired entries and add new ones -- daw, 10/23/17
|
||||
# switch to using curl, iptables-{save,restore}; 4/25/18
|
||||
# syslog "version";, refuse to run if not on public internet; 10/11/18
|
||||
# check status after iptables commands; syslog potential reason on file retrieval failure;
|
||||
# defend against iptables-restore errors; 10/12/18
|
||||
# begin ipset mod (indicate presence or absence of ipset in SUMN); 10/19/18
|
||||
# continue ipset mod (maintain LCSRDrop ipset); 10/23/18
|
||||
# finish ipset mod (if ipset present, remove LCSRDrop chain, don't maintain LCSRDrop chain,
|
||||
# and add LOG_AND_DROP chain using LCSRDrop ipset); 11/7/18
|
||||
# ipset might be in /sbin rather than /usr/sbin; 11/12/18
|
||||
# insert LOG_AND_DROP chain at beginning rather then end; 11/12/18
|
||||
# use wget if curl is not available; 12/7/18
|
||||
# save temp copy of iptables -L for debugging if removing chain in favor of ipset; 12/9/18
|
||||
# save a copy of ipset restore file if ipset restore fails; 12/11/18
|
||||
# use timeout for curl/wget if available; 1/17/19
|
||||
# avoid buggy timeout on Fedora Core 3 (dhcp2.srv.lcsr) ; 1/22/19
|
||||
# workaround for buggy timeout on Fedora Core 3 (dhcp2.srv.lcsr) ; 1/23/19
|
||||
# don't assume LOG_AND_DROP chain exists if LCSRDrop ipset does ; 4/21/19
|
||||
# install ipset if needed and available ; 4/25/19
|
||||
# add "-exist" to ipset del commands ; 4/25/19
|
||||
# make sure 0 return status from yum info means ipset is available ; 4/26/19
|
||||
# "yum info" needs "timeout -s 9" on Fedora Core 3 (dhcp2.srv.lcsr) ; 4/26/19
|
||||
# avoid flurry of errors due to simultaneous "iptables restart" ; 4/27/19
|
||||
# drop previous edit -- reschedule or eliminate "iptables restart" ; 5/7/19
|
||||
# fix apt/apt-get code; 8/12/19
|
||||
# add a clue when refusing to run; 8/13/19
|
||||
# remove s from https because old SSL library ; 6/23/21
|
||||
|
||||
# path to iptables
|
||||
export PATH=$PATH:/sbin # sbin needed fot apt on klinzhai.rutgers.edu
|
||||
IPTABLES="/sbin/iptables";
|
||||
IPSAVE="/sbin/iptables-save";
|
||||
IPRESTORE="/sbin/iptables-restore";
|
||||
if [ -e /usr/sbin/ipset ]; then IPSET=/usr/sbin/ipset ; fi
|
||||
if [ -e /sbin/ipset ]; then IPSET=/sbin/ipset ; fi
|
||||
CURL="`which curl`";
|
||||
WGET="`which wget`";
|
||||
if [ -e /usr/bin/timeout ]; then TIMEOUT=/usr/bin/timeout ; fi
|
||||
if [ -e /bin/timeout ]; then TIMEOUT=/bin/timeout ; fi
|
||||
if [ $TIMEOUT"x" != "x" ]; then
|
||||
TIMEOUT9="$TIMEOUT -s 9 15"
|
||||
TIMEOUT="$TIMEOUT 15"
|
||||
fi
|
||||
|
||||
# if timeout is broken here, don't use it
|
||||
# (switch within command confuses timeout on FC3)
|
||||
# I found a workaround. Put command into a file
|
||||
#$TIMEOUT echo x -x > /dev/null 2>&1
|
||||
#if [ $? -ne 0 ]; then TIMEOUT="" ; fi
|
||||
LOGGER="`which logger`";
|
||||
BASENAME="`which basename`";
|
||||
BASE="`$BASENAME $0`";
|
||||
TMP="/tmp/$BASE.$$";
|
||||
NOIPSETF=/tmp/$BASE.noipset
|
||||
DQ='"'
|
||||
|
||||
SUM=`which sum`
|
||||
# use SUMSEP to indicate whether using ipset
|
||||
SUMSEP='-'
|
||||
if [ $IPSET"x" != "x" ]; then SUMSEP="+"; fi
|
||||
SUMN=`$SUM $0 | sed "s; *;$SUMSEP;"`
|
||||
BASEDATE=`grep '; *[0-9]*/[0-9]*/[0-9]* *$' $0 | tail -1 | awk '{print $NF}'`
|
||||
VFILE=/tmp/$BASE.version
|
||||
|
||||
# list of known IPs
|
||||
URL="http://report.rutgers.edu/DROP/attackers";
|
||||
|
||||
# save local copy here
|
||||
FILE="/tmp/attackers.drop"
|
||||
|
||||
# iptables custom chain
|
||||
CHAIN="LCSRDrop";
|
||||
|
||||
# ipset custom set
|
||||
SETNAME="LCSRDrop";
|
||||
|
||||
# lockfile
|
||||
LOCKF=/tmp/lcsrdrop.lock
|
||||
|
||||
# create temp lockfile
|
||||
( echo $$ > $LOCKF.$$ ) > /dev/null 2>&1
|
||||
|
||||
# put that in real lockfile place of that doesn't exist already
|
||||
if [ ! -f $LOCKF ]; then
|
||||
/bin/mv $LOCKF.$$ $LOCKF > /dev/null 2>&1
|
||||
fi
|
||||
|
||||
# see who now has the lock
|
||||
if [ -f $LOCKF ]; then
|
||||
if [ -r $LOCKF ]; then
|
||||
LPID=`cat $LOCKF`
|
||||
else
|
||||
LPID=1 # if we cannot read file, use init's pid
|
||||
fi
|
||||
else
|
||||
LPID=1 # if we cannot create file, use init's pid
|
||||
fi
|
||||
|
||||
# if it's not us, try removing lockfile if that process no longer exists
|
||||
# and clean up after self
|
||||
if [ $$ != "$LPID" ]; then
|
||||
/bin/ps -p $LPID > /dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
/bin/rm -f $LOCKF > /dev/null 2>&1
|
||||
fi
|
||||
/bin/rm -f $LOCKF.$$
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if version changed. If so, log it
|
||||
if [ ! -e $VFILE ]; then touch $VFILE ; fi
|
||||
echo "$SUMN ($BASEDATE)" > $VFILE.new
|
||||
diff $VFILE $VFILE.new > /dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
LOGVERSION=1
|
||||
else
|
||||
LOGVERSION=0
|
||||
fi
|
||||
|
||||
# If the date has changed, log the version as well
|
||||
DN=`/bin/ls -l $VFILE $VFILE.new | awk '{print $6,$7}' | sort -u | wc -l`
|
||||
if [ $DN -ne 1 ]; then LOGVERSION=1 ; fi
|
||||
/bin/mv -f $VFILE.new $VFILE
|
||||
|
||||
if [ $LOGVERSION -eq 1 ]; then
|
||||
$LOGGER -t$BASE -pdaemon.err "SUMN=$SUMN ($BASEDATE)"
|
||||
echo `date` "SUMN=$SUMN ($BASEDATE)"
|
||||
fi
|
||||
|
||||
# commented R private net assert to test
|
||||
: <<'END'
|
||||
/sbin/ip addr show | egrep "128\.6\.|165\.230\." > /dev/null
|
||||
if [ $? -ne 0 ] && [ ! -e $0.run-in-private-IP-space ]; then
|
||||
# $LOGGER -t$BASE -pdaemon.err "refusing to run in private IP space"
|
||||
IP=`ifconfig -a | grep 'inet 172\.' | sed -e 's;.* inet ;;' -e 's; .*;;'`
|
||||
$LOGGER -t$BASE -pdaemon.err "refusing to run in private IP space ($IP)"
|
||||
echo $BASE need not be run on Rutgers private IP space
|
||||
exit 0
|
||||
fi
|
||||
END
|
||||
|
||||
# make sure ipset is installed if possible
|
||||
if [ $IPSET"x" == "x" -a ! -e $NOIPSETF ]; then
|
||||
if [ -e /usr/bin/apt ]; then APT=/usr/bin/apt ; fi
|
||||
if [ -e /bin/apt ]; then APT=/bin/apt ; fi
|
||||
# if [ -e /usr/bin/apt-get ]; then APTGET=/usr/bin/apt-get ; fi
|
||||
# if [ -e /bin/apt-get ]; then APTGET=/bin/apt ; fi
|
||||
if [ -e /usr/bin/yum ]; then YUM=/usr/bin/yum ; fi
|
||||
if [ -e /bin/yum ]; then YUM=/bin/yum ; fi
|
||||
if [ $APT"x" == "x" -a $YUM"x" == "x" ]; then
|
||||
$LOGGER -t$BASE -pdaemon.err "Neither apt nor yum found"
|
||||
fi
|
||||
# if [ $APT"x" != "x" -a $APTGET"x" != "x" ]; then
|
||||
if [ $APT"x" != "x" ]; then
|
||||
$APT show ipset > /dev/null 2>&1
|
||||
if [ $? -eq 0 ]; then
|
||||
# echo `date +%T` "Installing ipset using apt-get"
|
||||
# $LOGGER -t$BASE -pdaemon.err "Installing ipset using apt-get"
|
||||
# $APTGET install -y ipset
|
||||
echo `date +%T` "Installing ipset using apt"
|
||||
$LOGGER -t$BASE -pdaemon.err "Installing ipset using apt"
|
||||
# Avoid sometimes exiting install with status 100
|
||||
$APT update
|
||||
STATUS=$?
|
||||
if [ $STATUS -ne 0 ]; then
|
||||
$LOGGER -t$BASE -pdaemon.err "$APT update returned status $STATUS"
|
||||
fi
|
||||
$APT install -y ipset
|
||||
STATUS=$?
|
||||
if [ $STATUS -ne 0 ]; then
|
||||
# $LOGGER -t$BASE -pdaemon.err "$APTGET install -y ipset returned status $STATUS"
|
||||
$LOGGER -t$BASE -pdaemon.err "$APT install -y ipset returned status $STATUS"
|
||||
else
|
||||
if [ -e /usr/sbin/ipset ]; then IPSET=/usr/sbin/ipset ; fi
|
||||
if [ -e /sbin/ipset ]; then IPSET=/sbin/ipset ; fi
|
||||
fi
|
||||
else
|
||||
touch $NOIPSETF
|
||||
fi
|
||||
fi
|
||||
if [ $YUM"x" != "x" ]; then
|
||||
$TIMEOUT9 $YUM info ipset > $TMP 2>&1
|
||||
if [ $? -eq 0 ]; then
|
||||
grep ipset $TMP /dev/null 2>&1
|
||||
if [ $? -eq 0 ]; then
|
||||
echo `date +%T` "Installing ipset using yum"
|
||||
$LOGGER -t$BASE -pdaemon.err "Installing ipset using yum"
|
||||
$YUM install -y ipset
|
||||
STATUS=$?
|
||||
if [ $STATUS -ne 0 ]; then
|
||||
$LOGGER -t$BASE -pdaemon.err "$YUM install -y ipset returned status $STATUS"
|
||||
else
|
||||
if [ -e /usr/sbin/ipset ]; then IPSET=/usr/sbin/ipset ; fi
|
||||
if [ -e /sbin/ipset ]; then IPSET=/sbin/ipset ; fi
|
||||
fi
|
||||
else
|
||||
touch $NOIPSETF
|
||||
fi
|
||||
else
|
||||
touch $NOIPSETF
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# check to see if the chain already exists
|
||||
$IPTABLES -L $CHAIN -n > /dev/null 2>&1
|
||||
ILCSTATUS=$?
|
||||
if [ $ILCSTATUS -ne 0 ]; then
|
||||
# only create chain if ipset not present
|
||||
if [ $IPSET"x" == "x" ]; then
|
||||
echo `date +%T` "Chain $CHAIN not detected. Creating new chain...."
|
||||
|
||||
# create a new chain set
|
||||
$IPTABLES -N $CHAIN
|
||||
STATUS=$?
|
||||
if [ $STATUS -ne 0 ]; then
|
||||
$LOGGER -t$BASE -pdaemon.err "$IPTABLES -N $CHAIN returned status $STATUS"
|
||||
fi
|
||||
|
||||
# tie chain to input rules so it runs
|
||||
$IPTABLES -A INPUT -j $CHAIN
|
||||
STATUS=$?
|
||||
if [ $STATUS -ne 0 ]; then
|
||||
$LOGGER -t$BASE -pdaemon.err "$IPTABLES -A INPUT -j $CHAIN returned status $STATUS"
|
||||
fi
|
||||
|
||||
# don't allow this traffic through
|
||||
$IPTABLES -A FORWARD -j $CHAIN
|
||||
STATUS=$?
|
||||
if [ $STATUS -ne 0 ]; then
|
||||
$LOGGER -t$BASE -pdaemon.err "$IPTABLES -A FORWARD -j $CHAIN returned status $STATUS"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# chain exists -- remove it if ipset present
|
||||
if [ $IPSET"x" != "x" ]; then
|
||||
# Save an old copy of this for debugging, just in case...
|
||||
echo `date +%T` "Saving old copy of $IPSET -L -n"
|
||||
$LOGGER -t$BASE -pdaemon.err "Saving old copy of $IPSET -L -n"
|
||||
$IPTABLES -L -n > /tmp/$BASE.iptables-L-n 2>&1
|
||||
echo `date +%T` "$IPSET exists. Removing chain $CHAIN...."
|
||||
$IPTABLES -D FORWARD -j $CHAIN
|
||||
STATUS=$?
|
||||
if [ $STATUS -ne 0 ]; then
|
||||
$LOGGER -t$BASE -pdaemon.err "$IPTABLES -D FORWARD -j $CHAIN returned status $STATUS"
|
||||
fi
|
||||
|
||||
$IPTABLES -D INPUT -j $CHAIN
|
||||
STATUS=$?
|
||||
if [ $STATUS -ne 0 ]; then
|
||||
$LOGGER -t$BASE -pdaemon.err "$IPTABLES -D INPUT -j $CHAIN returned status $STATUS"
|
||||
fi
|
||||
|
||||
$IPTABLES -F $CHAIN
|
||||
STATUS=$?
|
||||
if [ $STATUS -ne 0 ]; then
|
||||
$LOGGER -t$BASE -pdaemon.err "$IPTABLES -F $CHAIN returned status $STATUS"
|
||||
fi
|
||||
|
||||
$IPTABLES -X $CHAIN
|
||||
STATUS=$?
|
||||
if [ $STATUS -ne 0 ]; then
|
||||
$LOGGER -t$BASE -pdaemon.err "$IPTABLES -X $CHAIN returned status $STATUS"
|
||||
fi
|
||||
fi
|
||||
fi;
|
||||
|
||||
# Create LCSRDrop ipset if it does not exist
|
||||
|
||||
if [ $IPSET"x" != "x" ]; then
|
||||
$IPSET list $SETNAME > /dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo `date +%T` "set $SETNAME not detected. Creating new set...."
|
||||
|
||||
# create new ipset set
|
||||
$IPSET create $SETNAME iphash
|
||||
STATUS=$?
|
||||
if [ $STATUS -ne 0 ]; then
|
||||
$LOGGER -t$BASE -pdaemon.err "$IPSET create $SETNAME iphash returned status $STATUS"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Make sure LOG_AND_DROP chain exists
|
||||
|
||||
# $IPTABLES -L LOG_AND_DROP -n > /dev/null 2>&1
|
||||
# On report.cs at 0700, this sometimes does not detect LOG_AND_DROP because of simultaneous "iptables restart"
|
||||
# No simple way to avoid this other than rescheduling (or removing) "iptables restart"
|
||||
$IPTABLES -L LOG_AND_DROP -n > $TMP 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo " error from $IPTABLES -L LOG_AND_DROP -n:"
|
||||
sed 's;.; &;' $TMP
|
||||
|
||||
echo `date +%T` "Chain LOG_AND_DROP not detected. Creating new chain...."
|
||||
|
||||
# create LOG_AND_DROP chain
|
||||
$IPTABLES -N LOG_AND_DROP
|
||||
STATUS=$?
|
||||
if [ $STATUS -ne 0 ]; then
|
||||
$LOGGER -t$BASE -pdaemon.err "$IPTABLES -N LOG_AND_DROP returned status $STATUS"
|
||||
fi
|
||||
|
||||
$IPTABLES -A LOG_AND_DROP -m limit --limit 10/min -j LOG --log-prefix "LCSRDropped: "
|
||||
STATUS=$?
|
||||
if [ $STATUS -ne 0 ]; then
|
||||
$LOGGER -t$BASE -pdaemon.err "$IPTABLES -A LOG_AND_DROP -m limit --limit 10/min -j LOG --log-prefix ${DQ}LCSRDropped: $DQ returned status $STATUS"
|
||||
fi
|
||||
|
||||
$IPTABLES -A LOG_AND_DROP -j REJECT
|
||||
STATUS=$?
|
||||
if [ $STATUS -ne 0 ]; then
|
||||
$LOGGER -t$BASE -pdaemon.err "$IPTABLES -A LOG_AND_DROP -j REJECT returned status $STATUS"
|
||||
fi
|
||||
|
||||
# $IPTABLES -A INPUT -m set --match-set $SETNAME src -j LOG_AND_DROP
|
||||
# Put this at the beginning rather than the end
|
||||
$IPTABLES -I INPUT -m set --match-set $SETNAME src -j LOG_AND_DROP
|
||||
STATUS=$?
|
||||
if [ $STATUS -ne 0 ]; then
|
||||
$LOGGER -t$BASE -pdaemon.err "$IPTABLES -I INPUT -m set --match-set $SETNAME src -j LOG_AND_DROP returned status $STATUS"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# make sure $FILE does not exist
|
||||
if [ -e $FILE ]; then
|
||||
/bin/mv -f $FILE $FILE.old
|
||||
fi;
|
||||
|
||||
# get a copy of the drop list
|
||||
echo `date +%T` "Retrieving copy of attackers data...."
|
||||
if [ $CURL"x" != "x" ]; then
|
||||
# $TIMEOUT $CURL -k -s $URL -o $FILE
|
||||
# Put command into file so buggy timeout won't see it under FC3
|
||||
echo $CURL -k -s $URL -o $FILE > $TMP
|
||||
$TIMEOUT /bin/sh $TMP
|
||||
CSTATUS=$?
|
||||
else
|
||||
# if curl doesn't exist, use wget
|
||||
$TIMEOUT $WGET -q --no-check-certificate -O $FILE $URL
|
||||
CSTATUS=$?
|
||||
fi
|
||||
|
||||
# make sure it has nothing but IPs
|
||||
grep -v '^[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*$' $FILE > /dev/null
|
||||
CJSTATUS=$?
|
||||
|
||||
# check to see if curl succeeded and did not get junk
|
||||
if [ $CSTATUS -eq 0 -a $CJSTATUS -eq 1 ]; then
|
||||
|
||||
/bin/rm -f $FILE.old
|
||||
|
||||
else
|
||||
# use old data
|
||||
/bin/mv -f $FILE $FILE.fail
|
||||
/bin/mv -f $FILE.old $FILE
|
||||
|
||||
STRING=` grep '<title>' $FILE.fail | sed -e 's;.*<title>;;' -e 's:<.*:; :'`
|
||||
|
||||
echo `date +%T` "Retrieval of attackers data failed. Reusing old data...."
|
||||
$LOGGER -t$BASE -pdaemon.err "Retrieval of attackers data failed ($STRING$CSTATUS,$CJSTATUS). Reusing old data...."
|
||||
fi;
|
||||
|
||||
# Maintain LCSRDrop ipset if ipset program exists
|
||||
|
||||
if [ $IPSET"x" != "x" ]; then
|
||||
$IPSET list $SETNAME > $TMP.ipset 2>&1
|
||||
STATUS=$?
|
||||
if [ $STATUS -ne 0 ]; then
|
||||
echo `date +%T` "$IPSET list $SETNAME returned status $STATUS"
|
||||
$LOGGER -t$BASE -pdaemon.err "$IPSET list $SETNAME returned status $STATUS"
|
||||
fi
|
||||
grep '^[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*$' $TMP.ipset | sort -u > $TMP.old
|
||||
sort -u $FILE > $TMP.new
|
||||
comm -23 $TMP.{old,new} > $TMP.remove
|
||||
comm -13 $TMP.{old,new} > $TMP.add
|
||||
sed "s;.;del -exist $SETNAME &;" $TMP.remove > $TMP.restore
|
||||
sed "s;.;add -exist $SETNAME &;" $TMP.add >> $TMP.restore
|
||||
if [ -s $TMP.restore ]; then
|
||||
echo `date +%T` "restoring new $SETNAME ipset...."
|
||||
$IPSET restore < $TMP.restore
|
||||
STATUS=$?
|
||||
if [ $STATUS -ne 0 ]; then
|
||||
$LOGGER -t$BASE -pdaemon.err "$IPSET restore < $TMP.restore returned status $STATUS"
|
||||
echo `date +%T` "Saving copy of failed restore file"
|
||||
$LOGGER -t$BASE -pdaemon.err "Saving copy of failed restore file"
|
||||
/bin/mv $TMP.restore /tmp/$BASE.ipset.restore.failed
|
||||
fi
|
||||
else
|
||||
echo `date +%T` "No changes $SETNAME ipset...."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Old code maintains LCSRDrop chain if ipset not present
|
||||
|
||||
if [ $IPSET"x" == "x" ]; then
|
||||
# save current iptables rules with/without LCSRDRop
|
||||
$IPSAVE > $TMP.old
|
||||
SSTATUS=$?
|
||||
if [ $SSTATUS -ne 0 ]; then
|
||||
$LOGGER -t$BASE -pdaemon.err "$IPSAVE > $TMP.old returned status $SSTATUS"
|
||||
fi
|
||||
$IPSAVE | grep -v '^-A LCSRDrop' > $TMP
|
||||
STATUS=$?
|
||||
if [ $STATUS -ne 0 ]; then
|
||||
$LOGGER -t$BASE -pdaemon.err "$IPSAVE | grep -v '^-A LCSRDrop' > $TMP returned status $STATUS"
|
||||
fi
|
||||
|
||||
# build new save file to load
|
||||
|
||||
CLINE=`grep -n "COMMIT" $TMP | tail -1 | sed 's;:.*;;'`
|
||||
(( CLINEm1=$CLINE-1 ))
|
||||
|
||||
head -$CLINEm1 $TMP > $TMP.new
|
||||
sed 's;.*;-A LCSRDrop -s &/32 -j DROP;' $FILE >> $TMP.new
|
||||
tail --lines=+$CLINE $TMP >> $TMP.new
|
||||
|
||||
diff $TMP.{old,new} > /dev/null
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
|
||||
echo `date +%T` "restoring new iptables rules...."
|
||||
# "restore" new iptables rules
|
||||
$IPRESTORE < $TMP.new
|
||||
STATUS=$?
|
||||
if [ $STATUS -ne 0 ]; then
|
||||
echo `date +%T` "restore failed"
|
||||
$LOGGER -t$BASE -pdaemon.err "$IPRESTORE < $TMP.new returned status $STATUS"
|
||||
if [ $SSTATUS -eq 0 ]; then
|
||||
echo " restoring to saved state"
|
||||
$LOGGER -t$BASE -pdaemon.err "Restoring to saved state"
|
||||
$IPRESTORE < $TMP.old
|
||||
STATUS=$?
|
||||
if [ $STATUS -ne 0 ]; then
|
||||
echo `date +%T` "that restore failed too"
|
||||
$LOGGER -t$BASE -pdaemon.err "$IPRESTORE < $TMP.old returned status $STATUS"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
else
|
||||
|
||||
echo `date +%T` "No changes in iptables rules...."
|
||||
|
||||
fi;
|
||||
fi
|
||||
|
||||
# remove lockfile (and possible leftover temp lockfile) and temp files
|
||||
/bin/rm -f $LOCKF $LOCKF.$$ $TMP*
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user