PotholeIQ / box-workflow.js
Varun10000's picture
PotholeIQ: initial deployable build (Box + Supabase, HF Spaces Docker)
45e997f
Raw
History Blame Contribute Delete
9.41 kB
'use strict';
const BOX_WORKFLOW_TEMPLATE = 'pothole_box_ops_v1';
function workflowPriorityFromSeverity(severity) {
const normalized = String(severity || '').toLowerCase();
if (normalized === 'critical') return 'urgent';
if (normalized === 'high' || normalized === 'large') return 'high';
if (normalized === 'medium') return 'medium';
return 'normal';
}
function buildInitialWorkflow(report = {}, boxConfig = {}) {
const aiStatus = String(report.aiReviewStatus || '').toLowerCase();
const aiResult = String(report.aiResult || report.aiPrediction || '').toLowerCase();
const needsManualReview = aiStatus.includes('manual') || aiStatus.includes('face');
const needsAttention = aiStatus === 'invalid_image';
return {
template: BOX_WORKFLOW_TEMPLATE,
currentStage: 'incoming_detection',
currentStageLabel: 'Incoming Detection',
status: needsManualReview || needsAttention ? 'Under Review' : 'Submitted',
queue: needsManualReview || needsAttention ? 'Manual Review' : 'Ops Intake',
nextAction: needsAttention
? 'Supervisor must validate the upload before enrichment'
: needsManualReview
? 'Supervisor must verify the image before enrichment'
: 'Run enrichment and create the case folder',
priority: workflowPriorityFromSeverity(aiResult),
needsManualReview,
triggerFolderId: boxConfig.folderId || '',
triggerFolderName: boxConfig.folderName || 'Incoming Detections',
submittedAt: report.submittedAt || new Date().toISOString(),
lastUpdatedAt: report.submittedAt || new Date().toISOString(),
};
}
function buildEnrichedWorkflow(report = {}, enrichment = {}, context = {}) {
const severityLevel = String(enrichment.severityLevel || '').trim() || 'Pending';
const duplicateCount = enrichment.duplicate?.duplicateCount || 0;
const linkedCases = Array.isArray(enrichment.duplicate?.linkedCaseIds)
? enrichment.duplicate.linkedCaseIds
: [];
const urgent = severityLevel.toLowerCase() === 'critical';
return {
template: BOX_WORKFLOW_TEMPLATE,
currentStage: 'supervisor_review',
currentStageLabel: 'Supervisor Review',
status: 'Under Review',
queue: urgent ? 'Emergency Dispatch' : 'Supervisor Review',
nextAction: urgent
? 'Assign an emergency crew and dispatch immediately'
: duplicateCount > 0
? 'Review duplicate cluster and confirm routing'
: 'Supervisor approve the case and send it to crew routing',
priority: workflowPriorityFromSeverity(severityLevel),
needsManualReview: String(report.aiReviewStatus || '').toLowerCase().includes('manual'),
severityLevel,
severityScore: enrichment.severityScore ?? '',
duplicateCount,
linkedCases: linkedCases.join(', '),
district: enrichment.district?.district || report.district || '',
ward: enrichment.district?.ward || report.ward || '',
maintenanceZone: enrichment.district?.maintenanceZone || report.maintenanceZone || '',
boxPath: context.folderPath || '',
rootFolderId: context.rootFolderId || '',
lastUpdatedAt: enrichment.enrichedAt || new Date().toISOString(),
};
}
function flattenWorkflowForBox(workflow = {}) {
return {
workflowTemplate: workflow.template || BOX_WORKFLOW_TEMPLATE,
workflowStage: workflow.currentStage || '',
workflowStageLabel: workflow.currentStageLabel || '',
workflowStatus: workflow.status || '',
workflowQueue: workflow.queue || '',
workflowNextAction: workflow.nextAction || '',
workflowPriority: workflow.priority || '',
workflowNeedsManualReview: workflow.needsManualReview ? 'true' : 'false',
workflowSeverityLevel: workflow.severityLevel || '',
workflowSeverityScore: workflow.severityScore ?? '',
workflowDuplicateCount: workflow.duplicateCount ?? '',
workflowLinkedCases: workflow.linkedCases || '',
workflowWard: workflow.ward || '',
workflowDistrict: workflow.district || '',
workflowMaintenanceZone: workflow.maintenanceZone || '',
workflowBoxPath: workflow.boxPath || '',
workflowLastUpdatedAt: workflow.lastUpdatedAt || '',
workflowTriggerFolderId: workflow.triggerFolderId || workflow.rootFolderId || '',
workflowTriggerFolderName: workflow.triggerFolderName || '',
};
}
function getWorkflowInputFields() {
return [
{ key: 'caseId', source: 'Citizen upload', purpose: 'Primary record key used across Box, ops review, and tracking.' },
{ key: 'submittedAt', source: 'Citizen upload', purpose: 'Timestamp for intake SLAs and queue sorting.' },
{ key: 'photoFileId', source: 'Box upload', purpose: 'Original pothole photo file to preview or move into the case folder.' },
{ key: 'sidecarFileId', source: 'Box upload', purpose: 'Original JSON sidecar used as the workflow payload.' },
{ key: 'address', source: 'Citizen upload', purpose: 'Road location shown to reviewers and crew dispatch.' },
{ key: 'areaLabel', source: 'Reverse geocode', purpose: 'Human-friendly locality label shown before official ward matching succeeds.' },
{ key: 'ward', source: 'GIS or reverse geocode', purpose: 'Routes the case into the correct Box subfolder and maintenance area.' },
{ key: 'district', source: 'GIS mapping', purpose: 'Maps the case to the review or engineering district.' },
{ key: 'maintenanceZone', source: 'GIS mapping', purpose: 'Directs dispatch to the correct crew zone.' },
{ key: 'duplicateStatus', source: 'Ops enrichment', purpose: 'Simple duplicate/new label used by external workflow decisions.' },
{ key: 'weatherStatus', source: 'Ops enrichment', purpose: 'Weather risk status stored with the case for triage and reporting.' },
{ key: 'potholeSize', source: 'Citizen upload AI', purpose: 'Human-readable pothole size used before and after enrichment.' },
{ key: 'aiReviewStatus', source: 'Citizen upload AI', purpose: 'Determines whether the case should enter manual review first.' },
{ key: 'aiResult', source: 'Citizen upload AI', purpose: 'Initial pothole size signal before enrichment recalculates severity.' },
{ key: 'severityScore', source: 'Ops enrichment', purpose: 'Numeric urgency score for routing, triage, and reporting.' },
{ key: 'severityLevel', source: 'Ops enrichment', purpose: 'Human-readable priority label for Box tasks and dashboards.' },
{ key: 'isDuplicate', source: 'Ops enrichment', purpose: 'Flags repeat reports so teams can merge or escalate clusters.' },
{ key: 'workflowStage', source: 'Workflow metadata', purpose: 'Stable machine field for the current lifecycle stage.' },
{ key: 'workflowNextAction', source: 'Workflow metadata', purpose: 'Plain-language instruction shown to the next owner.' },
];
}
function getWorkflowStages() {
return [
{
id: 'incoming_detection',
title: '1. Citizen intake and Box file storage',
use: 'The portal uploads the photo and JSON sidecar to Box, then upserts the same case into the backend queue.',
input: 'Folder ID from Settings, caseId, submittedAt, address, location, aiReviewStatus, potholeSize',
},
{
id: 'supervisor_review',
title: '2. Database-owned enrichment and review',
use: 'Ops Review and backend services calculate GIS, duplicates, weather, and severity, then save enriched artifacts back into Box.',
input: 'photoFileId, sidecarFileId, ward, district, maintenanceZone, duplicateStatus, weatherStatus, severityScore',
},
{
id: 'crew_dispatch',
title: '3. Database dispatch with Box-backed artifacts',
use: 'The backend records supervisor approval, authority routing, and crew dispatch while Box stores the generated notices and assignment files.',
input: 'severityLevel, duplicateStatus, workflowPriority, workflowStatus, maintenanceZone, assignedCrew',
},
{
id: 'resolved',
title: '4. Database closeout with Box proof storage',
use: 'Crew forms, after photos, reports, and audit outputs are saved by the backend and written back into Box for shared recordkeeping.',
input: 'resolvedAt, resolutionNotes, afterPhotoFileId, laborHours, materialsUsed, workflowStatus',
},
];
}
function buildWorkflowBlueprint(boxConfig = {}) {
return {
template: BOX_WORKFLOW_TEMPLATE,
triggerFolderId: boxConfig.folderId || '',
triggerFolderName: boxConfig.folderName || 'Incoming Detections',
automationUses: [
'Box as the content repository for intake, case folders, proof files, and reports',
'JSON sidecar payload uploaded by the citizen portal',
'Enriched metadata and readable summaries written back after backend processing',
'Backend and app screens own approvals, dispatch, authority notifications, and closeout',
],
inputFields: getWorkflowInputFields(),
stages: getWorkflowStages(),
metadataKeys: Object.keys(flattenWorkflowForBox({})),
};
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
BOX_WORKFLOW_TEMPLATE,
buildInitialWorkflow,
buildEnrichedWorkflow,
flattenWorkflowForBox,
getWorkflowInputFields,
getWorkflowStages,
buildWorkflowBlueprint,
};
}
if (typeof window !== 'undefined') {
window.BoxWorkflow = {
BOX_WORKFLOW_TEMPLATE,
buildInitialWorkflow,
buildEnrichedWorkflow,
flattenWorkflowForBox,
getWorkflowInputFields,
getWorkflowStages,
buildWorkflowBlueprint,
};
}