Compare commits

..

4 Commits

Author SHA1 Message Date
67ee8b86b8 revert 2026-08-21 10:46:59 -04:00
0e023e59f0 sync-to-svn.yaml fixes
All checks were successful
Sync to SVN / push-to-svn (push) Successful in 29s
2026-08-21 10:34:04 -04:00
b987b48a5f yaml file changes 2026-08-21 10:28:33 -04:00
045ce34398 feat: 2026/08/21 - Advanced Reports added 2026-08-21 10:20:52 -04:00
1681 changed files with 121681 additions and 7861 deletions

View File

@ -12,7 +12,7 @@ on:
push: push:
branches: branches:
- master - master
jobs: jobs:
push-to-svn: push-to-svn:
runs-on: self-hosted runs-on: self-hosted
@ -44,7 +44,7 @@ jobs:
--no-auth-cache \ --no-auth-cache \
--non-interactive \ --non-interactive \
--trust-server-cert \ --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 - name: Sync files to SVN working copy
run: | run: |
@ -74,4 +74,4 @@ jobs:
--no-auth-cache \ --no-auth-cache \
--non-interactive \ --non-interactive \
--trust-server-cert \ --trust-server-cert \
-m "Gitea CI sync from commit ${{ github.sha }} [ci skip]" -m "Gitea CI sync from commit ${{ github.sha }} [ci skip]"

View File

@ -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

View File

@ -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;
}

View File

@ -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>

View File

@ -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();
}
}

View File

@ -1,3 +0,0 @@
.pure-white {
color: #FFFFFF;
}

View File

@ -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() { }
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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();
}
}
}

View File

@ -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;
}

View File

@ -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

View File

@ -1,2 +0,0 @@
DEBUG = agm:*,maintainer:*

View File

@ -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

View File

@ -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

View File

@ -1 +0,0 @@
engine-strict=true

View File

@ -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 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

View File

@ -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 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

View File

@ -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,
}

View File

@ -1,10 +0,0 @@
[
{
"username": "vcmosquito@volusia.org",
"package": "ESS-2",
"trackingQty": 3,
"startDate": "26/03/2025",
"endDate": "26/03/2026",
"taxable": "N"
}
]

View File

@ -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
}

View File

@ -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,
}

View File

@ -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.

View File

@ -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.*

View File

@ -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)** |

View File

@ -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 (UTC3) |
| 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 ? satsIn100 : satsIn` |
| `correctionId` | `AppDetail.tslu` decoded | integer | `tslu > 100 ? tslu100 : tslu` |
| `waasId` | `AppDetail.calcodeFreq` decoded | integer | Only if `calcodeFreq` in 2000129999 |
| `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 09 = RPM pairs 1/2 through 9/10 (pump RPM channels)
> - Dry material: index 01 = AppRPM 1/2; index 23 = TarRPM 1/2; index 4 = GFC VIn; index 67 = Revs/Kg (× 0.453592 for Revs/Lb); index 89 = 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) | 120 per job |
| Raw trace records | Cursor on `_id`, default 500/page | 10K500K+ 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 ~519526 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 13 avgSpraySpeed import field, API key infra, job listing
Week 2 Steps 45 Session summary + raw trace records endpoints
Week 3 Steps 67 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).

View File

@ -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)** |

View File

@ -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.
------------------

View File

@ -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>&nbsp;(%)</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.

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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;
}
}

View File

@ -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)

View File

@ -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
}

View File

@ -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:

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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/*;
}

View File

@ -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

View File

@ -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/

View File

@ -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-----

View File

@ -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-----

View File

@ -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-----

View File

@ -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-----

View File

@ -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-----

View File

@ -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-----

View File

@ -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-----

View File

@ -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-----

View File

@ -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-----

View File

@ -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-----

View File

@ -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-----

View File

@ -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-----

View File

@ -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-----

View File

@ -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-----

View File

@ -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-----

View File

@ -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-----

View File

@ -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-----

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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-----

View File

@ -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-----

View File

@ -1 +0,0 @@
72437C49EFD12BF6AC91F89353004FCF03737A13

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"
}
]
}

View File

@ -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

View File

@ -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

View File

@ -1,2 +0,0 @@
#!/bin/sh
sudo tar -cpzf /home/trung/bk_$(date +\%Y\%m\%d).tgz /home/trung/temp

View File

@ -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 = '';
});

View File

@ -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": ""
}
]
}

View File

@ -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!

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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": ""
}
]
}

View File

@ -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": ""
}
]
}

View File

@ -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

View File

@ -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": ""
}
]
}

View File

@ -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": ""
}
]
}

View File

@ -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