xlsx_import nach master vor 2 Tagen zusammengeführt
| @@ -4,4 +4,9 @@ | |||
| /volumes | |||
| /codex | |||
| *nul | |||
| gittoken.txt | |||
| gittoken.txt | |||
| /_bmad | |||
| /bmad_output | |||
| /.agents | |||
| /.claude | |||
| /uploads | |||
| @@ -151,7 +151,7 @@ Class JurisdictionController | |||
| End sub | |||
| Public Sub Import | |||
| Public Sub Import | |||
| dim page_size : page_size = 10 | |||
| dim page_num : page_num = Choice(Len(Request.Querystring("page_num")) > 0, Request.Querystring("page_num"), 1) | |||
| @@ -163,168 +163,651 @@ Class JurisdictionController | |||
| Model.PageSize = page_size | |||
| Model.PageCount = page_count | |||
| 'Model.RecordCount = record_count | |||
| HTMLSecurity.SetAntiCSRFToken "JurisdictionImportForm" | |||
| %> <!--#include file="../../Views/Jurisdiction/import.asp"--> <% | |||
| End Sub | |||
| Public Sub ImportPost | |||
| Dim Upload: Set Upload = New FreeASPUpload | |||
| Public Sub ImportPost | |||
| If UCase(Request.ServerVariables("REQUEST_METHOD") & "") <> "POST" Then | |||
| Err.Raise 1, "JurisdictionController:ImportPost", "Action only responds to POST requests." | |||
| End If | |||
| Dim Upload : Set Upload = New FreeASPUpload | |||
| Dim uploadPath, uploadedFile, fileName, fileExt, fileSize | |||
| Dim maxFileSize, dotPos, recordCount | |||
| Dim maxFileSize, dotPos, nonce | |||
| Dim savedFileName, savedLocalFileName, savedPath | |||
| Dim workbookData, worksheetName, headerIndex, missingHeaders | |||
| Dim importToken, totalRows, duplicateCount | |||
| maxFileSize = 10485760 '10 MB in bytes | |||
| uploadPath = Server.MapPath("/uploads") | |||
| 'Parse upload data | |||
| Upload.Upload | |||
| 'Validate file upload | |||
| nonce = Upload.Form("nonce") | |||
| If Not HTMLSecurity.IsValidAntiCSRFToken("JurisdictionImportForm", nonce) Then | |||
| HTMLSecurity.ClearAntiCSRFToken "JurisdictionImportForm" | |||
| HTMLSecurity.SetAntiCSRFToken "JurisdictionImportForm" | |||
| SendImportJsonError "Invalid form state. Please refresh and try again." | |||
| Exit Sub | |||
| End If | |||
| HTMLSecurity.ClearAntiCSRFToken "JurisdictionImportForm" | |||
| HTMLSecurity.SetAntiCSRFToken "JurisdictionImportForm" | |||
| If Upload.FileExists("filename") = False Then | |||
| Flash.AddError "No file selected for upload." | |||
| MVC.RedirectToAction "Import" | |||
| SendImportJsonError "No file selected for upload." | |||
| Exit Sub | |||
| End If | |||
| Set uploadedFile = Upload.UploadedFiles("filename") | |||
| fileName = uploadedFile.FileName | |||
| fileSize = uploadedFile.Length | |||
| 'Extract file extension | |||
| dotPos = InStrRev(fileName, ".") | |||
| If dotPos > 0 Then | |||
| fileExt = LCase(Mid(fileName, dotPos)) | |||
| Else | |||
| fileExt = "" | |||
| End If | |||
| 'Validate file type | |||
| If fileExt <> ".csv" And fileExt <> ".txt" Then | |||
| Flash.AddError "Only .csv and .txt files are allowed." | |||
| MVC.RedirectToAction "Import" | |||
| If fileExt <> ".xlsx" Then | |||
| SendImportJsonError "Only .xlsx files are allowed." | |||
| Exit Sub | |||
| End If | |||
| 'Validate file size | |||
| If fileSize > maxFileSize Then | |||
| Flash.AddError "File size exceeds 10 MB limit." | |||
| MVC.RedirectToAction "Import" | |||
| SendImportJsonError "File size exceeds 10 MB limit." | |||
| Exit Sub | |||
| End If | |||
| 'Save the file to configured folder | |||
| Upload.Save uploadPath | |||
| 'Remove the first line of the CSV (non-header info line) | |||
| StripFirstLine uploadedFile.Path | |||
| EnsureFolderExists uploadPath | |||
| Upload.SaveOne uploadPath, 0, savedFileName, savedLocalFileName | |||
| savedPath = uploadPath & "\" & savedLocalFileName | |||
| 'Open CSV with Jet driver and iterate records | |||
| Dim conn, rs, connString, folderPath, csvFileName, fso | |||
| Set fso = Server.CreateObject("Scripting.FileSystemObject") | |||
| folderPath = fso.GetParentFolderName(uploadedFile.Path) | |||
| csvFileName = fso.GetFileName(uploadedFile.Path) | |||
| Set fso = Nothing | |||
| On Error Resume Next | |||
| workbookData = ReadWorkbookData(savedPath, worksheetName) | |||
| If Err.Number <> 0 Then | |||
| Dim workbookErr : workbookErr = Err.Description | |||
| Err.Clear | |||
| On Error GoTo 0 | |||
| DeleteFileIfExists savedPath | |||
| SendImportJsonError "Unable to read the XLSX workbook. Verify Microsoft ACE OLEDB is installed and the workbook is valid. " & workbookErr | |||
| Exit Sub | |||
| End If | |||
| On Error GoTo 0 | |||
| If Not WorkbookHasHeaderRows(workbookData) Then | |||
| DeleteFileIfExists savedPath | |||
| SendImportJsonError "The uploaded workbook must contain an information row and a header row." | |||
| Exit Sub | |||
| End If | |||
| Set headerIndex = BuildHeaderIndex(workbookData, 1) | |||
| missingHeaders = MissingRequiredHeaders(headerIndex) | |||
| If Len(missingHeaders) > 0 Then | |||
| DeleteFileIfExists savedPath | |||
| SendImportJsonError "Missing required header(s): " & missingHeaders | |||
| Exit Sub | |||
| End If | |||
| totalRows = CountImportRows(workbookData) | |||
| duplicateCount = CountDuplicateJCodes(workbookData, CLng(headerIndex.Item("JURISDICTION"))) | |||
| importToken = CreateImportToken() | |||
| InitializeImportSession importToken, workbookData, worksheetName, fileName, savedPath, totalRows, duplicateCount, headerIndex | |||
| SendImportJsonSuccess importToken, fileName, totalRows, duplicateCount | |||
| End Sub | |||
| Public Sub ImportProgress | |||
| Dim importToken : importToken = Trim(Request.QueryString("token") & "") | |||
| If Len(importToken) = 0 Then | |||
| SendImportJsonError "Missing import token." | |||
| Exit Sub | |||
| End If | |||
| If Not ImportExists(importToken) Then | |||
| SendImportJsonError "The requested import session was not found. Please upload the workbook again." | |||
| Exit Sub | |||
| End If | |||
| Dim phase : phase = GetImportValue(importToken, "Phase") | |||
| If phase <> "complete" And phase <> "error" Then | |||
| ProcessImportChunk importToken, 25 | |||
| End If | |||
| SendImportProgressJson importToken | |||
| End Sub | |||
| Dim fmtType | |||
| If fileExt = ".txt" Then | |||
| fmtType = "TabDelimited" | |||
| Private Sub ProcessImportChunk(importToken, maxRowsPerRequest) | |||
| Dim sessionKey : sessionKey = ImportSessionKey(importToken) | |||
| Dim workbookData : workbookData = Session(sessionKey & "Data") | |||
| Dim currentRow, lastRow, rowsProcessed | |||
| If Not IsArray(workbookData) Then | |||
| SetImportValue importToken, "Phase", "error" | |||
| SetImportValue importToken, "StatusMessage", "Workbook data is no longer available in session." | |||
| AppendImportError importToken, 0, "Workbook data is no longer available in session.", "" | |||
| Exit Sub | |||
| End If | |||
| currentRow = CLng(GetImportValue(importToken, "CurrentRow")) | |||
| lastRow = UBound(workbookData, 2) | |||
| rowsProcessed = 0 | |||
| If currentRow < 2 Then currentRow = 2 | |||
| SetImportValue importToken, "Phase", "processing" | |||
| Do While currentRow <= lastRow And rowsProcessed < maxRowsPerRequest | |||
| ProcessImportRow importToken, workbookData, currentRow | |||
| currentRow = currentRow + 1 | |||
| rowsProcessed = rowsProcessed + 1 | |||
| SetImportValue importToken, "CurrentRow", currentRow | |||
| SetImportValue importToken, "ProcessedRows", CLng(GetImportValue(importToken, "ProcessedRows")) + 1 | |||
| Loop | |||
| If currentRow > lastRow Then | |||
| SetImportValue importToken, "Phase", "complete" | |||
| SetImportValue importToken, "StatusMessage", "Import complete." | |||
| Session.Contents.Remove sessionKey & "Data" | |||
| DeleteFileIfExists GetImportValue(importToken, "FilePath") | |||
| Session.Contents.Remove sessionKey & "FilePath" | |||
| Else | |||
| fmtType = "Delimited" | |||
| SetImportValue importToken, "StatusMessage", "Processed " & GetImportValue(importToken, "ProcessedRows") & " of " & GetImportValue(importToken, "TotalRows") & " row(s)." | |||
| End If | |||
| End Sub | |||
| Private Sub ProcessImportRow(importToken, workbookData, rowIndex) | |||
| Dim county, jurisdictionText, jCode, jurisdictionName, mailingAddress, cityTownship, zipPlusFour, mailerId | |||
| Dim csz, imbDigits, imb, jurisdictionModel, recordSummary | |||
| county = SafeWorkbookValue(workbookData(GetImportColumnIndex(importToken, "CountyIndex"), rowIndex)) | |||
| jurisdictionText = SafeWorkbookValue(workbookData(GetImportColumnIndex(importToken, "JurisdictionIndex"), rowIndex)) | |||
| mailingAddress = SafeWorkbookValue(workbookData(GetImportColumnIndex(importToken, "MailingAddressIndex"), rowIndex)) | |||
| cityTownship = SafeWorkbookValue(workbookData(GetImportColumnIndex(importToken, "CityTownshipIndex"), rowIndex)) | |||
| zipPlusFour = SafeWorkbookValue(workbookData(GetImportColumnIndex(importToken, "ZipPlusFourIndex"), rowIndex)) | |||
| mailerId = SafeWorkbookValue(workbookData(GetImportColumnIndex(importToken, "MailerIdIndex"), rowIndex)) | |||
| recordSummary = BuildImportRecordSummary(county, jurisdictionText, mailingAddress, cityTownship, zipPlusFour, mailerId) | |||
| If Len(Trim(county & jurisdictionText & mailingAddress & cityTownship & zipPlusFour & mailerId)) = 0 Then | |||
| IncrementImportCounter importToken, "InvalidCount" | |||
| AppendImportError importToken, rowIndex + 1, "Row is empty.", recordSummary | |||
| Exit Sub | |||
| End If | |||
| jCode = ExtractJurisdictionCode(jurisdictionText) | |||
| If Len(jCode) = 0 Then | |||
| IncrementImportCounter importToken, "InvalidCount" | |||
| AppendImportError importToken, rowIndex + 1, "Jurisdiction field is missing a code in parentheses.", recordSummary | |||
| Exit Sub | |||
| End If | |||
| jurisdictionName = NormalizeJurisdictionName(ExtractJurisdictionName(jurisdictionText)) | |||
| If Len(jurisdictionName) = 0 Then | |||
| IncrementImportCounter importToken, "InvalidCount" | |||
| AppendImportError importToken, rowIndex + 1, "Jurisdiction name could not be parsed.", recordSummary | |||
| Exit Sub | |||
| End If | |||
| csz = BuildCSZ(cityTownship, zipPlusFour) | |||
| imbDigits = BuildIMBDigits(mailerId, zipPlusFour) | |||
| If Len(imbDigits) = 0 Then | |||
| IncrementImportCounter importToken, "InvalidCount" | |||
| AppendImportError importToken, rowIndex + 1, "Unable to build IMB digits from Mailer ID Option 1 and ZIP + 4.", recordSummary | |||
| Exit Sub | |||
| End If | |||
| connString = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" & folderPath & ";Extended Properties=""text;HDR=YES;FMT=" & fmtType & """;" | |||
| Set conn = Server.CreateObject("ADODB.Connection") | |||
| On Error Resume Next | |||
| conn.Open connString | |||
| imb = GetIMBCodec.EncodeDigits(imbDigits) | |||
| If Err.Number <> 0 Then | |||
| Flash.AddError "Unable to read CSV file." | |||
| MVC.RedirectToAction "Import" | |||
| Dim imbErr : imbErr = Err.Description | |||
| Err.Clear | |||
| On Error GoTo 0 | |||
| IncrementImportCounter importToken, "FailedCount" | |||
| AppendImportError importToken, rowIndex + 1, "IMB generation failed for JCode " & jCode & ". " & imbErr, recordSummary | |||
| Exit Sub | |||
| End If | |||
| On Error Goto 0 | |||
| Dim sql | |||
| sql = "SELECT q.[County], q.[Jurisdiction], q.JCODE, " & _ | |||
| "IIf(InStr(1, UCase(q.NameRaw), ' CITY') > 0, " & _ | |||
| "'CITY OF ' & Trim(Replace(UCase(q.NameRaw), ' CITY', '')), " & _ | |||
| "q.NameRaw) AS [Name], " & _ | |||
| "q.[Mailing Address] AS Mailing_Address, " & _ | |||
| "q.[City & Township] & ' ' & q.[ZIP + 4] AS CSZ, " & _ | |||
| "'' AS IMB, " & _ | |||
| "'007' & q.[Mailer ID Option 1] & '000000' & Replace(q.[ZIP + 4], '-', '') AS IMB_Digits " & _ | |||
| "FROM (" & _ | |||
| "SELECT [County], [Jurisdiction], [Mailing Address], [City & Township], [ZIP + 4], [Mailer ID Option 1], " & _ | |||
| "IIf(InStr(1,[Jurisdiction],'(') > 0 And InStr(1,[Jurisdiction],')') > InStr(1,[Jurisdiction],'('), " & _ | |||
| "Trim(Mid([Jurisdiction], InStr(1,[Jurisdiction],'(') + 1, InStr(1,[Jurisdiction],')') - InStr(1,[Jurisdiction],'(') - 1)), " & _ | |||
| "Null) AS JCODE, " & _ | |||
| "IIf(InStr(1,[Jurisdiction],'(') > 0, " & _ | |||
| "Trim(Left([Jurisdiction], InStr(1,[Jurisdiction],'(') - 1)), " & _ | |||
| "Trim([Jurisdiction])) AS NameRaw " & _ | |||
| "FROM [" & csvFileName & "]) AS q" | |||
| Set rs = conn.Execute(sql) | |||
| recordCount = 0 | |||
| Do While Not rs.EOF | |||
| Dim jurisdiction : Set jurisdiction = New JurisdictionModel_Class | |||
| jurisdiction.JCode = Trim(rs.Fields("JCODE").Value & "") | |||
| jurisdiction.Name = Trim(rs.Fields("Name").Value & "") | |||
| jurisdiction.Mailing_Address = Trim(rs.Fields("Mailing_Address").Value & "") | |||
| jurisdiction.CSZ = Trim(rs.Fields("CSZ").Value & "") | |||
| jurisdiction.IMB_Digits = Trim(rs.Fields("IMB_Digits").Value & "") | |||
| jurisdiction.IMB = GetIMBCodec.EncodeDigits(jurisdiction.IMB_Digits) | |||
| 'JurisdictionRepository.AddNew jurisdiction | |||
| recordCount = recordCount + 1 | |||
| rs.MoveNext | |||
| Loop | |||
| On Error GoTo 0 | |||
| On Error Resume Next | |||
| Set jurisdictionModel = JurisdictionRepository.FindByJCode(jCode) | |||
| If Err.Number <> 0 Then | |||
| Err.Clear | |||
| On Error GoTo 0 | |||
| Set jurisdictionModel = New JurisdictionModel_Class | |||
| jurisdictionModel.JCode = jCode | |||
| jurisdictionModel.Name = jurisdictionName | |||
| jurisdictionModel.Mailing_Address = mailingAddress | |||
| jurisdictionModel.CSZ = csz | |||
| jurisdictionModel.IMB_Digits = imbDigits | |||
| jurisdictionModel.IMB = imb | |||
| On Error Resume Next | |||
| JurisdictionRepository.AddNewWithJCode jurisdictionModel | |||
| If Err.Number <> 0 Then | |||
| Dim insertErr : insertErr = Err.Description | |||
| Err.Clear | |||
| On Error GoTo 0 | |||
| IncrementImportCounter importToken, "FailedCount" | |||
| AppendImportError importToken, rowIndex + 1, "Failed to insert new JCode " & jCode & ". " & insertErr, recordSummary | |||
| Exit Sub | |||
| End If | |||
| On Error GoTo 0 | |||
| IncrementImportCounter importToken, "InsertedCount" | |||
| Exit Sub | |||
| End If | |||
| On Error GoTo 0 | |||
| jurisdictionModel.Name = jurisdictionName | |||
| jurisdictionModel.Mailing_Address = mailingAddress | |||
| jurisdictionModel.CSZ = csz | |||
| jurisdictionModel.IMB_Digits = imbDigits | |||
| jurisdictionModel.IMB = imb | |||
| On Error Resume Next | |||
| JurisdictionRepository.Update jurisdictionModel | |||
| If Err.Number <> 0 Then | |||
| Dim updateErr : updateErr = Err.Description | |||
| Err.Clear | |||
| On Error GoTo 0 | |||
| IncrementImportCounter importToken, "FailedCount" | |||
| AppendImportError importToken, rowIndex + 1, "Failed to update JCode " & jCode & ". " & updateErr, recordSummary | |||
| Exit Sub | |||
| End If | |||
| On Error GoTo 0 | |||
| IncrementImportCounter importToken, "UpdatedCount" | |||
| End Sub | |||
| Private Function ReadWorkbookData(filePath, ByRef worksheetName) | |||
| Dim conn, rs, connString, sql | |||
| connString = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" & filePath & ";Extended Properties=""Excel 12.0 Xml;HDR=NO;IMEX=1"";" | |||
| Set conn = Server.CreateObject("ADODB.Connection") | |||
| conn.Open connString | |||
| worksheetName = FirstWorksheetName(conn) | |||
| If Len(worksheetName) = 0 Then | |||
| conn.Close | |||
| Set conn = Nothing | |||
| Err.Raise vbObjectError + 2010, "JurisdictionController", "No worksheet was found in the uploaded workbook." | |||
| End If | |||
| sql = "SELECT * FROM [" & worksheetName & "]" | |||
| Set rs = Server.CreateObject("ADODB.Recordset") | |||
| rs.Open sql, conn, 3, 1 | |||
| If rs.EOF And rs.BOF Then | |||
| rs.Close | |||
| conn.Close | |||
| Set rs = Nothing | |||
| Set conn = Nothing | |||
| Err.Raise vbObjectError + 2011, "JurisdictionController", "The worksheet does not contain any rows." | |||
| End If | |||
| ReadWorkbookData = rs.GetRows() | |||
| rs.Close | |||
| conn.Close | |||
| Set rs = Nothing | |||
| Set conn = Nothing | |||
| End Function | |||
| Private Function FirstWorksheetName(conn) | |||
| Dim schema, tableName | |||
| Set schema = conn.OpenSchema(20) | |||
| Do Until schema.EOF | |||
| tableName = Trim(schema("TABLE_NAME") & "") | |||
| If InStr(tableName, "$") > 0 And InStr(tableName, "_xlnm") = 0 Then | |||
| FirstWorksheetName = tableName | |||
| schema.Close | |||
| Set schema = Nothing | |||
| Exit Function | |||
| End If | |||
| schema.MoveNext | |||
| Loop | |||
| 'Set success message with record count | |||
| Flash.Success = "File '" & fileName & "' uploaded successfully. Records imported: " & recordCount | |||
| MVC.RedirectToAction "Import" | |||
| If Not schema Is Nothing Then | |||
| schema.Close | |||
| Set schema = Nothing | |||
| End If | |||
| FirstWorksheetName = "" | |||
| End Function | |||
| Private Function BuildHeaderIndex(workbookData, headerRowIndex) | |||
| Dim headerMap, colIndex, headerName | |||
| Set headerMap = Server.CreateObject("Scripting.Dictionary") | |||
| For colIndex = 0 To UBound(workbookData, 1) | |||
| headerName = UCase(Trim(SafeWorkbookValue(workbookData(colIndex, headerRowIndex)))) | |||
| If Len(headerName) > 0 Then | |||
| If Not headerMap.Exists(headerName) Then | |||
| headerMap.Add headerName, colIndex | |||
| End If | |||
| End If | |||
| Next | |||
| Set BuildHeaderIndex = headerMap | |||
| End Function | |||
| Private Function WorkbookHasHeaderRows(workbookData) | |||
| On Error Resume Next | |||
| WorkbookHasHeaderRows = (IsArray(workbookData) And UBound(workbookData, 2) >= 1) | |||
| If Err.Number <> 0 Then | |||
| Err.Clear | |||
| WorkbookHasHeaderRows = False | |||
| End If | |||
| On Error GoTo 0 | |||
| End Function | |||
| Private Function MissingRequiredHeaders(headerIndex) | |||
| Dim requiredHeaders, missingHeaders, i | |||
| requiredHeaders = Array("County", "Jurisdiction", "Mailing Address", "City & Township", "ZIP + 4", "Mailer ID Option 1") | |||
| missingHeaders = "" | |||
| For i = 0 To UBound(requiredHeaders) | |||
| If Not headerIndex.Exists(UCase(requiredHeaders(i))) Then | |||
| If Len(missingHeaders) > 0 Then missingHeaders = missingHeaders & ", " | |||
| missingHeaders = missingHeaders & requiredHeaders(i) | |||
| End If | |||
| Next | |||
| MissingRequiredHeaders = missingHeaders | |||
| End Function | |||
| Private Function CountImportRows(workbookData) | |||
| If Not WorkbookHasHeaderRows(workbookData) Then | |||
| CountImportRows = 0 | |||
| ElseIf UBound(workbookData, 2) < 2 Then | |||
| CountImportRows = 0 | |||
| Else | |||
| CountImportRows = UBound(workbookData, 2) - 1 | |||
| End If | |||
| End Function | |||
| Private Function CountDuplicateJCodes(workbookData, jurisdictionColumnIndex) | |||
| Dim seenCodes, rowIndex, jCode | |||
| Set seenCodes = Server.CreateObject("Scripting.Dictionary") | |||
| CountDuplicateJCodes = 0 | |||
| If Not WorkbookHasHeaderRows(workbookData) Then Exit Function | |||
| If UBound(workbookData, 2) < 2 Then Exit Function | |||
| For rowIndex = 2 To UBound(workbookData, 2) | |||
| jCode = ExtractJurisdictionCode(SafeWorkbookValue(workbookData(jurisdictionColumnIndex, rowIndex))) | |||
| If Len(jCode) > 0 Then | |||
| If seenCodes.Exists(jCode) Then | |||
| CountDuplicateJCodes = CountDuplicateJCodes + 1 | |||
| Else | |||
| seenCodes.Add jCode, True | |||
| End If | |||
| End If | |||
| Next | |||
| End Function | |||
| Private Sub InitializeImportSession(importToken, workbookData, worksheetName, fileName, filePath, totalRows, duplicateCount, headerIndex) | |||
| Dim sessionKey : sessionKey = ImportSessionKey(importToken) | |||
| Session(sessionKey & "Data") = workbookData | |||
| Session(sessionKey & "WorksheetName") = worksheetName | |||
| Session(sessionKey & "FileName") = fileName | |||
| Session(sessionKey & "FilePath") = filePath | |||
| Session(sessionKey & "CurrentRow") = 2 | |||
| Session(sessionKey & "ProcessedRows") = 0 | |||
| Session(sessionKey & "TotalRows") = totalRows | |||
| Session(sessionKey & "UpdatedCount") = 0 | |||
| Session(sessionKey & "InsertedCount") = 0 | |||
| Session(sessionKey & "UnmatchedCount") = 0 | |||
| Session(sessionKey & "InvalidCount") = 0 | |||
| Session(sessionKey & "FailedCount") = 0 | |||
| Session(sessionKey & "DuplicateCount") = duplicateCount | |||
| Session(sessionKey & "Phase") = "staged" | |||
| Session(sessionKey & "StatusMessage") = "Workbook uploaded. Processing will begin shortly." | |||
| Session(sessionKey & "Errors") = "" | |||
| Session(sessionKey & "CountyIndex") = CLng(headerIndex.Item("COUNTY")) | |||
| Session(sessionKey & "JurisdictionIndex") = CLng(headerIndex.Item("JURISDICTION")) | |||
| Session(sessionKey & "MailingAddressIndex") = CLng(headerIndex.Item("MAILING ADDRESS")) | |||
| Session(sessionKey & "CityTownshipIndex") = CLng(headerIndex.Item("CITY & TOWNSHIP")) | |||
| Session(sessionKey & "ZipPlusFourIndex") = CLng(headerIndex.Item("ZIP + 4")) | |||
| Session(sessionKey & "MailerIdIndex") = CLng(headerIndex.Item("MAILER ID OPTION 1")) | |||
| End Sub | |||
| Private Function CreateImportToken() | |||
| Dim token : token = HTMLSecurity.Nonce() | |||
| token = Replace(token, "{", "") | |||
| token = Replace(token, "}", "") | |||
| CreateImportToken = token | |||
| End Function | |||
| Private Function ImportSessionKey(importToken) | |||
| ImportSessionKey = "JurisdictionImport." & importToken & "." | |||
| End Function | |||
| Private Function ImportExists(importToken) | |||
| ImportExists = (Len(GetImportValue(importToken, "Phase")) > 0) | |||
| End Function | |||
| Private Function GetImportValue(importToken, name) | |||
| GetImportValue = Session(ImportSessionKey(importToken) & name) | |||
| End Function | |||
| Private Sub SetImportValue(importToken, name, value) | |||
| Session(ImportSessionKey(importToken) & name) = value | |||
| End Sub | |||
| Private Sub StripFirstLine(filePath) | |||
| Dim fso, ts, remainingText | |||
| Set fso = Server.CreateObject("Scripting.FileSystemObject") | |||
| If Not fso.FileExists(filePath) Then | |||
| Set fso = Nothing | |||
| Exit Sub | |||
| Private Function GetImportColumnIndex(importToken, name) | |||
| GetImportColumnIndex = CLng(GetImportValue(importToken, name)) | |||
| End Function | |||
| Private Sub IncrementImportCounter(importToken, counterName) | |||
| SetImportValue importToken, counterName, CLng(GetImportValue(importToken, counterName)) + 1 | |||
| End Sub | |||
| Private Function ExtractJurisdictionCode(jurisdictionText) | |||
| Dim openPos, closePos | |||
| openPos = InStrRev(jurisdictionText, "(") | |||
| closePos = InStrRev(jurisdictionText, ")") | |||
| If openPos > 0 And closePos > openPos Then | |||
| ExtractJurisdictionCode = Trim(Mid(jurisdictionText, openPos + 1, closePos - openPos - 1)) | |||
| Else | |||
| ExtractJurisdictionCode = "" | |||
| End If | |||
| End Function | |||
| Set ts = fso.OpenTextFile(filePath, 1) ' 1 = ForReading | |||
| If ts.AtEndOfStream Then | |||
| ts.Close | |||
| Set ts = Nothing | |||
| Set fso = Nothing | |||
| Exit Sub | |||
| Private Function ExtractJurisdictionName(jurisdictionText) | |||
| Dim openPos | |||
| openPos = InStrRev(jurisdictionText, "(") | |||
| If openPos > 0 Then | |||
| ExtractJurisdictionName = Trim(Left(jurisdictionText, openPos - 1)) | |||
| Else | |||
| ExtractJurisdictionName = Trim(jurisdictionText) | |||
| End If | |||
| End Function | |||
| Private Function NormalizeJurisdictionName(jurisdictionName) | |||
| Dim normalizedName | |||
| normalizedName = Trim(jurisdictionName) | |||
| If Len(normalizedName) >= 5 Then | |||
| If UCase(Right(normalizedName, 5)) = " CITY" Then | |||
| normalizedName = "CITY OF " & Trim(Left(normalizedName, Len(normalizedName) - 5)) | |||
| End If | |||
| End If | |||
| 'Skip the first line | |||
| ts.ReadLine | |||
| NormalizeJurisdictionName = normalizedName | |||
| End Function | |||
| 'Read the rest of the file | |||
| If Not ts.AtEndOfStream Then | |||
| remainingText = ts.ReadAll | |||
| Private Function BuildCSZ(cityTownship, zipPlusFour) | |||
| BuildCSZ = Trim(Trim(cityTownship) & " " & Trim(zipPlusFour)) | |||
| End Function | |||
| Private Function BuildIMBDigits(mailerId, zipPlusFour) | |||
| Dim cleanMailerId, cleanZip | |||
| cleanMailerId = DigitsOnly(mailerId) | |||
| cleanZip = DigitsOnly(zipPlusFour) | |||
| If Len(cleanMailerId) = 0 Or Len(cleanZip) = 0 Then | |||
| BuildIMBDigits = "" | |||
| Else | |||
| BuildIMBDigits = "00778" & cleanMailerId & "000000" & cleanZip | |||
| End If | |||
| End Function | |||
| Private Function DigitsOnly(inputValue) | |||
| Dim i, ch | |||
| DigitsOnly = "" | |||
| For i = 1 To Len(inputValue) | |||
| ch = Mid(inputValue, i, 1) | |||
| If ch >= "0" And ch <= "9" Then | |||
| DigitsOnly = DigitsOnly & ch | |||
| End If | |||
| Next | |||
| End Function | |||
| Private Function SafeWorkbookValue(value) | |||
| If IsNull(value) Or IsEmpty(value) Then | |||
| SafeWorkbookValue = "" | |||
| Else | |||
| SafeWorkbookValue = Trim(CStr(value)) | |||
| End If | |||
| End Function | |||
| Private Function BuildImportRecordSummary(county, jurisdictionText, mailingAddress, cityTownship, zipPlusFour, mailerId) | |||
| BuildImportRecordSummary = "County=" & DisplayImportField(county) & _ | |||
| "; Jurisdiction=" & DisplayImportField(jurisdictionText) & _ | |||
| "; Mailing Address=" & DisplayImportField(mailingAddress) & _ | |||
| "; City & Township=" & DisplayImportField(cityTownship) & _ | |||
| "; ZIP + 4=" & DisplayImportField(zipPlusFour) & _ | |||
| "; Mailer ID Option 1=" & DisplayImportField(mailerId) | |||
| End Function | |||
| Private Function DisplayImportField(value) | |||
| If Len(Trim(value & "")) = 0 Then | |||
| DisplayImportField = "<blank>" | |||
| Else | |||
| DisplayImportField = value & "" | |||
| End If | |||
| End Function | |||
| Private Sub AppendImportError(importToken, rowNumber, errorMessage, recordSummary) | |||
| Dim sessionKey, currentErrors, fullMessage | |||
| sessionKey = ImportSessionKey(importToken) | |||
| currentErrors = Session(sessionKey & "Errors") & "" | |||
| If rowNumber > 0 Then | |||
| fullMessage = "Row " & rowNumber & ": " & errorMessage | |||
| Else | |||
| fullMessage = errorMessage | |||
| End If | |||
| If Len(Trim(recordSummary & "")) > 0 Then | |||
| fullMessage = fullMessage & " Record: " & recordSummary | |||
| End If | |||
| If Len(currentErrors) > 0 Then | |||
| currentErrors = currentErrors & Chr(30) | |||
| End If | |||
| currentErrors = currentErrors & fullMessage | |||
| Session(sessionKey & "Errors") = currentErrors | |||
| End Sub | |||
| Private Sub SendImportJsonSuccess(importToken, fileName, totalRows, duplicateCount) | |||
| Response.ContentType = "application/json" | |||
| Response.Write "{""ok"":true,""token"":""" & JsonEscape(importToken) & """,""fileName"":""" & JsonEscape(fileName) & """,""phase"":""staged"",""totalRows"":" & totalRows & ",""processedRows"":0,""updatedCount"":0,""insertedCount"":0,""invalidCount"":0,""failedCount"":0,""duplicateCount"":" & duplicateCount & ",""percentComplete"":0,""statusMessage"":""Workbook uploaded. Processing will begin shortly."",""nextNonce"":""" & JsonEscape(HTMLSecurity.GetAntiCSRFToken("JurisdictionImportForm")) & """,""errors"":[]}" | |||
| Response.End | |||
| End Sub | |||
| Private Sub SendImportProgressJson(importToken) | |||
| Dim phase, totalRows, processedRows, updatedCount, insertedCount, invalidCount, failedCount, duplicateCount | |||
| Dim percentComplete, statusMessage, errorsJson | |||
| phase = GetImportValue(importToken, "Phase") | |||
| totalRows = CLng(GetImportValue(importToken, "TotalRows")) | |||
| processedRows = CLng(GetImportValue(importToken, "ProcessedRows")) | |||
| updatedCount = CLng(GetImportValue(importToken, "UpdatedCount")) | |||
| insertedCount = CLng(GetImportValue(importToken, "InsertedCount")) | |||
| invalidCount = CLng(GetImportValue(importToken, "InvalidCount")) | |||
| failedCount = CLng(GetImportValue(importToken, "FailedCount")) | |||
| duplicateCount = CLng(GetImportValue(importToken, "DuplicateCount")) | |||
| statusMessage = GetImportValue(importToken, "StatusMessage") & "" | |||
| errorsJson = JsonArrayFromDelimitedString(GetImportValue(importToken, "Errors") & "") | |||
| If totalRows > 0 Then | |||
| percentComplete = Int((processedRows / totalRows) * 100) | |||
| Else | |||
| remainingText = "" | |||
| percentComplete = 100 | |||
| End If | |||
| If phase = "complete" Then percentComplete = 100 | |||
| Response.ContentType = "application/json" | |||
| Response.Write "{""ok"":true,""token"":""" & JsonEscape(importToken) & """,""phase"":""" & JsonEscape(phase) & """,""totalRows"":" & totalRows & ",""processedRows"":" & processedRows & ",""updatedCount"":" & updatedCount & ",""insertedCount"":" & insertedCount & ",""invalidCount"":" & invalidCount & ",""failedCount"":" & failedCount & ",""duplicateCount"":" & duplicateCount & ",""percentComplete"":" & percentComplete & ",""statusMessage"":""" & JsonEscape(statusMessage) & """,""errors"":" & errorsJson & "}" | |||
| Response.End | |||
| End Sub | |||
| Private Sub SendImportJsonError(errorMessage) | |||
| Response.ContentType = "application/json" | |||
| Response.Status = "400 Bad Request" | |||
| Response.Write "{""ok"":false,""message"":""" & JsonEscape(errorMessage) & """,""nextNonce"":""" & JsonEscape(HTMLSecurity.GetAntiCSRFToken("JurisdictionImportForm")) & """}" | |||
| Response.End | |||
| End Sub | |||
| Private Function JsonArrayFromDelimitedString(delimitedValue) | |||
| Dim items, i, result | |||
| If Len(delimitedValue) = 0 Then | |||
| JsonArrayFromDelimitedString = "[]" | |||
| Exit Function | |||
| End If | |||
| ts.Close | |||
| 'Rewrite the file without the first line | |||
| Set ts = fso.OpenTextFile(filePath, 2) ' 2 = ForWriting | |||
| ts.Write remainingText | |||
| ts.Close | |||
| items = Split(delimitedValue, Chr(30)) | |||
| result = "[" | |||
| For i = 0 To UBound(items) | |||
| If i > 0 Then result = result & "," | |||
| result = result & """" & JsonEscape(items(i)) & """" | |||
| Next | |||
| result = result & "]" | |||
| JsonArrayFromDelimitedString = result | |||
| End Function | |||
| Private Function JsonEscape(value) | |||
| value = Replace(value & "", "\", "\\") | |||
| value = Replace(value, """", "\""") | |||
| value = Replace(value, vbCrLf, "\n") | |||
| value = Replace(value, vbCr, "\n") | |||
| value = Replace(value, vbLf, "\n") | |||
| JsonEscape = value | |||
| End Function | |||
| Private Sub EnsureFolderExists(folderPath) | |||
| Dim fso : Set fso = Server.CreateObject("Scripting.FileSystemObject") | |||
| If Not fso.FolderExists(folderPath) Then | |||
| fso.CreateFolder folderPath | |||
| End If | |||
| Set fso = Nothing | |||
| End Sub | |||
| Private Sub DeleteFileIfExists(filePath) | |||
| Dim fso | |||
| If Len(Trim(filePath & "")) = 0 Then Exit Sub | |||
| Set ts = Nothing | |||
| Set fso = Server.CreateObject("Scripting.FileSystemObject") | |||
| If fso.FileExists(filePath) Then | |||
| On Error Resume Next | |||
| fso.DeleteFile filePath, True | |||
| On Error GoTo 0 | |||
| End If | |||
| Set fso = Nothing | |||
| End Sub | |||
| End Class | |||
| @@ -205,6 +205,23 @@ Class JurisdictionRepository_Class | |||
| Destroy rs | |||
| End Sub | |||
| Public Sub AddNewWithJCode(ByRef model) | |||
| dim sql : sql = "INSERT INTO [Jurisdiction] (" &_ | |||
| "[JCode]," &_ | |||
| "[Name]," &_ | |||
| "[Mailing_Address]," &_ | |||
| "[CSZ]," &_ | |||
| "[IMB]," &_ | |||
| "[IMB_Digits])" &_ | |||
| "VALUES (?,?,?,?,?,?)" | |||
| DAL.Execute sql, Array(model.JCode, _ | |||
| model.Name, _ | |||
| model.Mailing_Address, _ | |||
| model.CSZ, _ | |||
| model.IMB, _ | |||
| model.IMB_Digits) | |||
| End Sub | |||
| Public Sub Update(model) | |||
| dim sql : sql = "UPDATE [Jurisdiction] SET [Name] = ?," &_ | |||
| "[Mailing_Address] = ?," &_ | |||
| @@ -3,23 +3,250 @@ | |||
| <% Flash().ShowErrorsIfPresent %> | |||
| <% Flash().ShowSuccessIfPresent %> | |||
| <%= HTML.FormTag("Jurisdiction", "ImportPost", empty, Array("enctype","multipart/form-data")) %> | |||
| <%= HTML.FormTag("Jurisdiction", "ImportPost", Array("_P","1"), Array("enctype","multipart/form-data","id","jurisdiction-import-form")) %> | |||
| <%= HTML.Hidden("nonce", HTMLSecurity.GetAntiCSRFToken("JurisdictionImportForm")) %> | |||
| <hr /> | |||
| <div><p>Upload a CSV (.csv) or tab-delimited (.txt) file to import jurisdiction data</p></div> | |||
| <div> | |||
| <p>Upload the BRM permit workbook as an Excel file. Row 1 is ignored, row 2 must contain the expected headers, and jurisdictions are updated or inserted using the JCode inside the <code>Jurisdiction</code> column.</p> | |||
| </div> | |||
| <div class="form-group"> | |||
| <div class="row"> | |||
| <div class="col-md-4"> | |||
| <div class="col-md-5"> | |||
| <div class="form-group"> | |||
| <label for="filename">Select File</label> | |||
| <input type="file" id="filename" name="filename" accept=".csv,.txt" required> | |||
| <label for="filename">Select XLSX File</label> | |||
| <input type="file" id="filename" name="filename" accept=".xlsx" required class="form-control"> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <p></p> | |||
| <%= HTML.Button("submit", "<i class='glyphicon glyphicon-ok'></i> Upload", "btn-primary") %> | |||
| <small class="form-text text-muted">Accepted formats: .csv, .txt | Max size: 10 MB</small> | |||
| <button type="submit" id="import-submit" class="btn btn-primary"><i class="glyphicon glyphicon-upload"></i> Start Import</button> | |||
| <small class="form-text text-muted">Accepted format: .xlsx | Max size: 10 MB</small> | |||
| </div> | |||
| </form> | |||
| <div id="import-feedback" class="panel panel-default" style="display:none; margin-top:20px;"> | |||
| <div class="panel-heading"> | |||
| <strong>Import Progress</strong> | |||
| </div> | |||
| <div class="panel-body"> | |||
| <p id="import-status">Waiting to start.</p> | |||
| <div class="progress"> | |||
| <div id="import-progress-bar" class="progress-bar progress-bar-striped active" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" style="width:0%;"> | |||
| 0% | |||
| </div> | |||
| </div> | |||
| <div class="row"> | |||
| <div class="col-sm-2"><strong>Total</strong><div id="summary-total">0</div></div> | |||
| <div class="col-sm-2"><strong>Processed</strong><div id="summary-processed">0</div></div> | |||
| <div class="col-sm-2"><strong>Updated</strong><div id="summary-updated">0</div></div> | |||
| <div class="col-sm-2"><strong>Inserted</strong><div id="summary-inserted">0</div></div> | |||
| <div class="col-sm-2"><strong>Invalid</strong><div id="summary-invalid">0</div></div> | |||
| <div class="col-sm-2"><strong>Failed</strong><div id="summary-failed">0</div></div> | |||
| </div> | |||
| <div class="row" style="margin-top:12px;"> | |||
| <div class="col-sm-2"><strong>Duplicates</strong><div id="summary-duplicates">0</div></div> | |||
| </div> | |||
| <div id="import-error-panel" class="alert alert-warning" style="display:none; margin-top:15px;"> | |||
| <strong>Row Issues</strong> | |||
| <ul id="import-error-list" style="margin-top:10px; margin-bottom:0;"></ul> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <div id="import-message" class="alert" style="display:none; margin-top:20px;"></div> | |||
| <script type="text/javascript"> | |||
| (function () { | |||
| var form = document.getElementById('jurisdiction-import-form'); | |||
| var fileInput = document.getElementById('filename'); | |||
| var submitButton = document.getElementById('import-submit'); | |||
| var nonceField = form.querySelector('input[name="nonce"]'); | |||
| var feedbackPanel = document.getElementById('import-feedback'); | |||
| var messageBox = document.getElementById('import-message'); | |||
| var statusText = document.getElementById('import-status'); | |||
| var progressBar = document.getElementById('import-progress-bar'); | |||
| var pollTimer = null; | |||
| var summaryFields = { | |||
| totalRows: document.getElementById('summary-total'), | |||
| processedRows: document.getElementById('summary-processed'), | |||
| updatedCount: document.getElementById('summary-updated'), | |||
| insertedCount: document.getElementById('summary-inserted'), | |||
| invalidCount: document.getElementById('summary-invalid'), | |||
| failedCount: document.getElementById('summary-failed'), | |||
| duplicateCount: document.getElementById('summary-duplicates') | |||
| }; | |||
| var errorPanel = document.getElementById('import-error-panel'); | |||
| var errorList = document.getElementById('import-error-list'); | |||
| var pollUrlBase = '<%= Routes.UrlTo("Jurisdiction", "ImportProgress", Array("_P","1")) %>'; | |||
| function setMessage(cssClass, message) { | |||
| messageBox.className = 'alert ' + cssClass; | |||
| messageBox.textContent = message; | |||
| messageBox.style.display = 'block'; | |||
| } | |||
| function clearMessage() { | |||
| messageBox.style.display = 'none'; | |||
| messageBox.textContent = ''; | |||
| } | |||
| function updateSummary(data) { | |||
| summaryFields.totalRows.textContent = data.totalRows || 0; | |||
| summaryFields.processedRows.textContent = data.processedRows || 0; | |||
| summaryFields.updatedCount.textContent = data.updatedCount || 0; | |||
| summaryFields.insertedCount.textContent = data.insertedCount || 0; | |||
| summaryFields.invalidCount.textContent = data.invalidCount || 0; | |||
| summaryFields.failedCount.textContent = data.failedCount || 0; | |||
| summaryFields.duplicateCount.textContent = data.duplicateCount || 0; | |||
| } | |||
| function refreshNonce(data) { | |||
| if (nonceField && data && data.nextNonce) { | |||
| nonceField.value = data.nextNonce; | |||
| } | |||
| } | |||
| function updateProgress(data) { | |||
| var percent = data.percentComplete || 0; | |||
| progressBar.style.width = percent + '%'; | |||
| progressBar.setAttribute('aria-valuenow', percent); | |||
| progressBar.textContent = percent + '%'; | |||
| statusText.textContent = data.statusMessage || 'Processing import.'; | |||
| updateSummary(data); | |||
| renderErrors(data.errors || []); | |||
| if (data.phase === 'complete') { | |||
| progressBar.className = 'progress-bar'; | |||
| setMessage('alert-success', 'Jurisdiction import complete.'); | |||
| } else if (data.phase === 'error') { | |||
| progressBar.className = 'progress-bar'; | |||
| setMessage('alert-danger', data.statusMessage || 'Import failed.'); | |||
| } | |||
| } | |||
| function renderErrors(errors) { | |||
| errorList.innerHTML = ''; | |||
| if (!errors.length) { | |||
| errorPanel.style.display = 'none'; | |||
| return; | |||
| } | |||
| for (var i = 0; i < errors.length; i += 1) { | |||
| var item = document.createElement('li'); | |||
| item.textContent = errors[i]; | |||
| errorList.appendChild(item); | |||
| } | |||
| errorPanel.style.display = 'block'; | |||
| } | |||
| function stopPolling() { | |||
| if (pollTimer) { | |||
| window.clearTimeout(pollTimer); | |||
| pollTimer = null; | |||
| } | |||
| submitButton.disabled = false; | |||
| } | |||
| function sendJsonRequest(url, options, onSuccess, onError) { | |||
| var xhr = new XMLHttpRequest(); | |||
| xhr.open(options.method || 'GET', url, true); | |||
| xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); | |||
| xhr.onreadystatechange = function () { | |||
| var data; | |||
| if (xhr.readyState !== 4) { | |||
| return; | |||
| } | |||
| try { | |||
| data = JSON.parse(xhr.responseText || '{}'); | |||
| } catch (parseError) { | |||
| onError('The server returned an invalid JSON response.'); | |||
| return; | |||
| } | |||
| if (xhr.status < 200 || xhr.status >= 300 || !data.ok) { | |||
| refreshNonce(data); | |||
| onError((data && data.message) || 'The request failed.'); | |||
| return; | |||
| } | |||
| refreshNonce(data); | |||
| onSuccess(data); | |||
| }; | |||
| xhr.onerror = function () { | |||
| onError('The request failed.'); | |||
| }; | |||
| xhr.send(options.body || null); | |||
| } | |||
| function pollProgress(token) { | |||
| sendJsonRequest( | |||
| pollUrlBase + '&token=' + encodeURIComponent(token), | |||
| { method: 'GET' }, | |||
| function (data) { | |||
| updateProgress(data); | |||
| if (data.phase === 'complete' || data.phase === 'error') { | |||
| stopPolling(); | |||
| return; | |||
| } | |||
| pollTimer = window.setTimeout(function () { | |||
| pollProgress(token); | |||
| }, 1200); | |||
| }, | |||
| function (error) { | |||
| stopPolling(); | |||
| setMessage('alert-danger', error || 'Import progress request failed.'); | |||
| } | |||
| ); | |||
| } | |||
| form.addEventListener('submit', function (event) { | |||
| event.preventDefault(); | |||
| clearMessage(); | |||
| renderErrors([]); | |||
| if (!fileInput.files.length) { | |||
| setMessage('alert-danger', 'Select an .xlsx file before starting the import.'); | |||
| return; | |||
| } | |||
| submitButton.disabled = true; | |||
| feedbackPanel.style.display = 'block'; | |||
| statusText.textContent = 'Uploading workbook...'; | |||
| progressBar.className = 'progress-bar progress-bar-striped active'; | |||
| updateSummary({ | |||
| totalRows: 0, | |||
| processedRows: 0, | |||
| updatedCount: 0, | |||
| insertedCount: 0, | |||
| invalidCount: 0, | |||
| failedCount: 0, | |||
| duplicateCount: 0, | |||
| percentComplete: 0 | |||
| }); | |||
| updateProgress({ percentComplete: 0, statusMessage: 'Uploading workbook...', errors: [] }); | |||
| var formData = new FormData(form); | |||
| sendJsonRequest( | |||
| form.action, | |||
| { method: 'POST', body: formData }, | |||
| function (data) { | |||
| updateProgress(data); | |||
| pollProgress(data.token); | |||
| }, | |||
| function (error) { | |||
| stopPolling(); | |||
| setMessage('alert-danger', error || 'Import request failed.'); | |||
| } | |||
| ); | |||
| }); | |||
| })(); | |||
| </script> | |||
| @@ -1,14 +1,81 @@ | |||
| FROM node:20-bookworm | |||
| FROM node:lts | |||
| # Install GitHub Copilot CLI | |||
| RUN npm install -g @github/copilot | |||
| # Install PHP, Python, DB clients, and general utilities | |||
| RUN apt-get update && apt-get install -y --no-install-recommends \ | |||
| php php-cli php-common php-mbstring php-xml php-curl php-zip \ | |||
| php-pgsql php-mysql php-sqlite3 php-redis \ | |||
| python3 python3-pip python3-venv \ | |||
| wget apt-transport-https \ | |||
| git make jq unzip zip \ | |||
| openssh-client \ | |||
| default-mysql-client postgresql-client sqlite3 \ | |||
| && rm -rf /var/lib/apt/lists/* | |||
| # Install useful dev tools | |||
| RUN apt-get update && apt-get install -y \ | |||
| git \ | |||
| curl \ | |||
| # Install Docker CLI | |||
| RUN curl -fsSL https://download.docker.com/linux/debian/gpg \ | |||
| | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \ | |||
| && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo $VERSION_CODENAME) stable" \ | |||
| | tee /etc/apt/sources.list.d/docker.list > /dev/null \ | |||
| && apt-get update && apt-get install -y --no-install-recommends docker-ce-cli docker-compose-plugin \ | |||
| && rm -rf /var/lib/apt/lists/* | |||
| WORKDIR /workspace | |||
| # Install GitHub CLI | |||
| RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \ | |||
| | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \ | |||
| && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ | |||
| | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ | |||
| && apt-get update && apt-get install -y --no-install-recommends gh \ | |||
| && rm -rf /var/lib/apt/lists/* | |||
| # Install Composer | |||
| RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer | |||
| CMD ["bash"] | |||
| # Install Python tools | |||
| RUN pip install --break-system-packages poetry uv | |||
| # Install Spec Kit (specify CLI) | |||
| RUN uv tool install specify-cli --from git+https://github.com/github/spec-kit.git | |||
| # Install .NET 10 | |||
| RUN wget https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb -O packages-microsoft-prod.deb \ | |||
| && dpkg -i packages-microsoft-prod.deb \ | |||
| && rm packages-microsoft-prod.deb \ | |||
| && apt-get update && apt-get install -y --no-install-recommends dotnet-sdk-10.0 \ | |||
| && rm -rf /var/lib/apt/lists/* | |||
| # Install PowerShell | |||
| RUN apt-get update && apt-get install -y --no-install-recommends powershell \ | |||
| && rm -rf /var/lib/apt/lists/* | |||
| # Install .NET global tools | |||
| RUN dotnet tool install --global dotnet-ef \ | |||
| && dotnet tool install --global dotnet-aspnet-codegenerator | |||
| ENV PATH="$PATH:/root/.dotnet/tools:/root/.local/bin" | |||
| RUN git config --global user.name "Daniel Covington" | |||
| RUN git config --global user.email "danielcovington@comcast.net" | |||
| # Install Node.js global tools | |||
| RUN npm install -g @bonsai-ai/cli | |||
| RUN npm install -g @abacus-ai/cli | |||
| RUN npm install -g @openai/codex | |||
| RUN npm install -g @github/copilot | |||
| # Install ttyd (web terminal) | |||
| RUN ARCH=$(dpkg --print-architecture) && \ | |||
| case "$ARCH" in \ | |||
| amd64) TTYD_ARCH="x86_64" ;; \ | |||
| arm64) TTYD_ARCH="aarch64" ;; \ | |||
| *) echo "Unsupported arch: $ARCH" && exit 1 ;; \ | |||
| esac && \ | |||
| wget -O /usr/local/bin/ttyd \ | |||
| "https://github.com/tsl0922/ttyd/releases/latest/download/ttyd.${TTYD_ARCH}" && \ | |||
| chmod +x /usr/local/bin/ttyd | |||
| # Install BMAD-METHOD CLI | |||
| RUN npm install -g bmad-method | |||
| WORKDIR /workspace | |||
| EXPOSE 7681 | |||
| CMD ["ttyd", "-W", "-p", "7681", "bash"] | |||
| @@ -120,7 +120,7 @@ set fs = Server.CreateObject("Scripting.FileSystemObject") | |||
| streamFile.SaveToFile path & outLocalFileName, 2 | |||
| streamFile.close | |||
| Set streamFile = Nothing | |||
| fileItem.Path = path & filename | |||
| fileItem.Path = path & outLocalFileName | |||
| end if | |||
| end sub | |||
| @@ -402,4 +402,4 @@ NewFullPath = strSaveToPath & "\" & strTempFileName & Counter & "." & FileExt | |||
| End If | |||
| Loop | |||
| End Function | |||
| %> | |||
| %> | |||
| @@ -0,0 +1,188 @@ | |||
| --- | |||
| title: 'Add XLSX Jurisdiction Import' | |||
| slug: 'add-xlsx-jurisdiction-import' | |||
| created: '2026-03-13' | |||
| status: 'implemented' | |||
| stepsCompleted: [1, 2, 3, 4] | |||
| tech_stack: ['Classic ASP', 'VBScript', 'IIS', 'ADO', 'Microsoft Jet/ACE OLEDB', 'FreeASPUpload', 'Session-backed MVC helpers'] | |||
| files_to_modify: ['/workspace/App/Controllers/Jurisdiction/JurisdictionController.asp', '/workspace/App/Views/Jurisdiction/import.asp', '/workspace/App/DomainModels/JurisdictionRepository.asp'] | |||
| code_patterns: ['Controller action with inline file-processing logic', 'Repository-based persistence with raw SQL and Automapper models', 'Session-backed Flash/FormCache/CSRF helpers', 'Server-rendered forms with HTML helper methods'] | |||
| test_patterns: ['ASPUnit helper tests only', 'No existing controller/import coverage', 'Manual IIS verification required for upload/import flows'] | |||
| --- | |||
| # Tech-Spec: Add XLSX Jurisdiction Import | |||
| **Created:** 2026-03-13 | |||
| ## Overview | |||
| ### Problem Statement | |||
| The current jurisdiction import only supports `.csv` and `.txt`, relies on the Jet text driver, and does not persist the imported jurisdiction updates. The business now needs an `.xlsx`-only import flow for a known spreadsheet format that updates the `Jurisdiction` table and gives the user progress feedback while the import runs. | |||
| ### Solution | |||
| Replace the current jurisdiction import implementation with an XLSX-based import pipeline that reads a fixed workbook format, skips row 1, uses row 2 as headers, extracts `JCode` and normalized `Name` from the `Jurisdiction` column, joins `City & Township` and `ZIP + 4` into `CSZ`, regenerates `IMB_Digits` and `IMB` using existing logic, updates existing `Jurisdiction` rows by extracted `JCode`, inserts missing `JCode` rows, and exposes AJAX progress updates in the import UI. | |||
| ### Scope | |||
| **In Scope:** | |||
| - Replace the current import UI and backend flow with `.xlsx`-only support | |||
| - Support the known workbook shape represented by `Data\BRM Permit Info February '26 updated.xlsx` | |||
| - Ignore row 1 and treat row 2 as the header row | |||
| - Extract `JCode` from text inside parentheses in the `Jurisdiction` column | |||
| - Normalize `Name` from the text before parentheses | |||
| - If the normalized name ends with `CITY`, transform it to `CITY OF <name without trailing CITY>` | |||
| - Build `CSZ` from `City & Township` plus `ZIP + 4` | |||
| - Update `Mailing_Address`, `CSZ`, `IMB_Digits`, and `IMB` | |||
| - Regenerate `IMB` through the existing `GetIMBCodec.EncodeDigits(...)` path | |||
| - Add AJAX-driven progress feedback to the import screen | |||
| **Out of Scope:** | |||
| - Supporting `.csv` or `.txt` imports on the jurisdiction screen | |||
| - Supporting arbitrary workbook layouts or multiple worksheet formats | |||
| - Broad refactors to unrelated jurisdiction CRUD features | |||
| ## Context for Development | |||
| ### Codebase Patterns | |||
| - The current jurisdiction import lives in `JurisdictionController.ImportPost` and uses `FreeASPUpload` plus server-side file processing. | |||
| - The current CSV/TXT path already contains business logic for `JCode`, `Name`, `CSZ`, `IMB_Digits`, and `IMB` derivation. | |||
| - The repo is a Classic ASP / VBScript application running on IIS with existing shared helpers in `MVC/`. | |||
| - Environment-sensitive behavior should stay minimal and localized because this codebase is tightly coupled to Windows/IIS/runtime dependencies. | |||
| - Controller methods are allowed to contain substantial workflow logic in this codebase; there is no separate service layer pattern to follow. | |||
| - Persistence updates should still flow through `JurisdictionRepository` instead of ad hoc SQL inside views. | |||
| - Session-backed helpers already exist for flash state and CSRF tokens, which makes Session a viable anchor for import progress state if the implementation stays inside the web app. | |||
| - `MVC/lib.json.asp` exists, so lightweight JSON responses for AJAX polling fit the current stack without introducing a new dependency. | |||
| ### Files to Reference | |||
| | File | Purpose | | |||
| | ---- | ------- | | |||
| | `/workspace/App/Controllers/Jurisdiction/JurisdictionController.asp` | Current import entry point and existing CSV/TXT derivation logic | | |||
| | `/workspace/App/Views/Jurisdiction/import.asp` | Current upload UI to be changed for XLSX and progress feedback | | |||
| | `/workspace/App/DomainModels/JurisdictionRepository.asp` | Jurisdiction lookup and update persistence path | | |||
| | `/workspace/MVC/lib.Upload.asp` | Existing upload helper used by controller imports | | |||
| | `/workspace/MVC/lib.json.asp` | Existing JSON helper if the progress endpoint returns polling status | | |||
| | `/workspace/Data/BRM Permit Info February '26 updated.xlsx` | Sample workbook shape to anchor header and column mapping | | |||
| | `/workspace/_bmad-output/project-context.md` | Brownfield implementation rules and constraints | | |||
| ### Technical Decisions | |||
| - The import screen will become XLSX-only. | |||
| - Matching is by extracted `JCode` from the `Jurisdiction` column. | |||
| - The implementation must preserve and reuse the existing IMB encoding path rather than inventing a new barcode algorithm. | |||
| - Progress feedback is required and should be exposed through AJAX rather than a synchronous full-page post only. | |||
| - The current Jet text-driver import path cannot read `.xlsx`; the implementation needs a Windows/IIS-compatible Excel-reading strategy. | |||
| - Primary workbook-read strategy should use `Microsoft.ACE.OLEDB.12.0` on Windows/IIS. If ACE is unavailable, the import should fail with a clear user-facing error. | |||
| - The controller will likely need to split import work into at least two responsibilities: kickoff/upload and progress/status reporting. | |||
| - The repository currently supports `FindByJCode` and `Update`, which is enough for update-by-`JCode` if the controller loads and mutates each row model before persisting. | |||
| - There is no existing automated controller/import test pattern, so verification will rely on manual IIS-based import testing plus any helper-level tests that can be isolated. | |||
| - The import page should use a kickoff action plus an AJAX polling endpoint. Progress state can be stored in `Session` under a generated import token. | |||
| - Because this runs under Classic ASP/IIS, true live per-row progress may be constrained by request/session locking behavior. The implementation should use a two-phase design if needed and may expose checkpoint-based progress if that is the most reliable bounded solution. | |||
| - Required workbook headers should be validated before processing begins: `County`, `Jurisdiction`, `Mailing Address`, `City & Township`, `ZIP + 4`, and `Mailer ID Option 1`. | |||
| - Rows with extracted `JCode` values not found in `Jurisdiction` should be inserted and counted separately in the final summary. | |||
| - If duplicate `JCode` rows appear in the workbook, process them in file order and let the last valid row win while counting duplicates in the final summary. | |||
| - Name normalization should trim whitespace and apply the `CITY` rewrite only when the extracted name ends with the standalone word `CITY`. | |||
| - Row-level failures should be isolated, counted, and reported in the completion summary rather than aborting the entire import after partial progress. | |||
| - Progress state should remain session-scoped rather than being persisted to the database or temp files. | |||
| - Persistence should prefer the existing `FindByJCode` + mutate + `JurisdictionRepository.Update` flow before introducing a new repository bulk-update abstraction. | |||
| - Header binding should validate the expected row-2 header names and then resolve columns by header names rather than raw column positions only. | |||
| - The final import summary should include at minimum: total rows seen, rows updated, rows inserted, duplicate rows encountered, invalid rows skipped, and row-level failures. | |||
| - The final result should also expose row-level operator feedback with row number, failure reason, and full imported record details for malformed `Jurisdiction` values, `IMB_Digits` build failures, ACE read failures, and other row-level processing errors. | |||
| ## Implementation Plan | |||
| ### Tasks | |||
| - [x] Task 1: Replace the current jurisdiction import contract with XLSX-only validation | |||
| - File: `/workspace/App/Controllers/Jurisdiction/JurisdictionController.asp` | |||
| - Action: Update `ImportPost` so it only accepts `.xlsx` uploads, removes the CSV/TXT-specific text-driver path, and returns a clear validation error for any non-XLSX file. | |||
| - Notes: Keep the existing `FreeASPUpload` flow for multipart upload handling unless the ACE-based workbook read requires a different saved-file path convention. | |||
| - [x] Task 2: Add workbook-reading and header-validation logic for the known spreadsheet format | |||
| - File: `/workspace/App/Controllers/Jurisdiction/JurisdictionController.asp` | |||
| - Action: Read the uploaded `.xlsx` workbook through `Microsoft.ACE.OLEDB.12.0`, target the first worksheet, ignore row 1, treat row 2 as headers, and validate the required headers before row processing begins. | |||
| - Notes: Fail fast with a user-facing error if ACE is unavailable, the workbook cannot be opened, or required headers are missing. | |||
| - [x] Task 3: Implement row-mapping and normalization rules for jurisdiction updates | |||
| - File: `/workspace/App/Controllers/Jurisdiction/JurisdictionController.asp` | |||
| - Action: For each workbook row, extract `JCode` from the `Jurisdiction` value inside parentheses, derive normalized `Name` from the text before parentheses, apply the `CITY` to `CITY OF ...` rewrite when appropriate, join `City & Township` and `ZIP + 4` into `CSZ`, compute `IMB_Digits`, and regenerate `IMB` via `GetIMBCodec.EncodeDigits(...)`. | |||
| - Notes: Trim whitespace on all derived values and treat malformed or unparseable rows as row-level failures instead of fatal process errors. | |||
| - [x] Task 4: Persist updates by existing `JCode` and track import result counters | |||
| - File: `/workspace/App/Controllers/Jurisdiction/JurisdictionController.asp` | |||
| - File: `/workspace/App/DomainModels/JurisdictionRepository.asp` | |||
| - Action: For each valid parsed row, load the existing jurisdiction by `JCode`, mutate the model fields, and call `JurisdictionRepository.Update`; insert missing `JCode` rows with the parsed values; detect duplicates in workbook order and allow the last valid row to win. | |||
| - Notes: Only add repository support if a small helper materially reduces controller duplication; otherwise stay with the existing `FindByJCode` + `Update` pattern. | |||
| - [x] Task 5: Introduce bounded progress-state tracking and a polling endpoint | |||
| - File: `/workspace/App/Controllers/Jurisdiction/JurisdictionController.asp` | |||
| - Action: Split the import flow into a kickoff path and a progress/status path, storing progress state in `Session` under an import token and returning JSON-compatible status for AJAX polling. | |||
| - Notes: If Classic ASP session locking prevents true live row-by-row progress, expose checkpoint-based phases such as `uploaded`, `opening workbook`, `validating headers`, `processing rows`, and `complete`. | |||
| - [x] Task 6: Replace the import page UI with XLSX-only upload and progress display | |||
| - File: `/workspace/App/Views/Jurisdiction/import.asp` | |||
| - Action: Update the form copy and file input to XLSX-only, add client-side AJAX kickoff/polling behavior, render a progress bar/status area, and display final summary counts plus row-level error details. | |||
| - Notes: The UX should remain usable if progress is phase-based rather than exact per-row percentage. | |||
| - [x] Task 7: Define the final operator summary and failure reporting | |||
| - File: `/workspace/App/Controllers/Jurisdiction/JurisdictionController.asp` | |||
| - File: `/workspace/App/Views/Jurisdiction/import.asp` | |||
| - Action: Return or render a completion result that includes total rows seen, rows updated, rows inserted, duplicate rows encountered, invalid rows skipped, row-level failures, and row-specific error details. | |||
| - Notes: Include row number, failure reason, and full record details for malformed `Jurisdiction` values, IMB digit failures, insert/update failures, and workbook-read errors where applicable. | |||
| - [x] Task 8: Update user-facing messaging and implementation notes for runtime dependency risk | |||
| - File: `/workspace/App/Views/Jurisdiction/import.asp` | |||
| - File: `/workspace/App/Controllers/Jurisdiction/JurisdictionController.asp` | |||
| - Action: Make sure operator-facing errors clearly describe ACE dependency issues and workbook validation failures. | |||
| - Notes: This keeps deployment/runtime failures diagnosable without digging through server logs first. | |||
| ### Acceptance Criteria | |||
| - [x] AC 1: Given the user opens the jurisdiction import page, when the screen renders, then it only advertises and accepts `.xlsx` uploads. | |||
| - [x] AC 2: Given a non-`.xlsx` file is submitted, when the import starts, then the user receives a validation error and no jurisdiction rows are changed. | |||
| - [x] AC 3: Given the uploaded workbook cannot be opened through ACE/OLEDB, when the import starts, then the user receives a clear workbook/ACE error and no jurisdiction rows are changed. | |||
| - [x] AC 4: Given the workbook is missing one of the required row-2 headers, when validation runs, then the import stops before row processing and reports the missing header(s). | |||
| - [x] AC 5: Given a row contains `Jurisdiction` text in the format `ALCONA TOWNSHIP (01040)`, when the row is processed, then `JCode` is parsed as `01040` and `Name` is parsed as `ALCONA TOWNSHIP`. | |||
| - [x] AC 6: Given a parsed jurisdiction name ends with the standalone word `CITY`, when normalization runs, then the stored `Name` becomes `CITY OF <name without trailing CITY>`. | |||
| - [x] AC 7: Given a workbook row has `City & Township` and `ZIP + 4` values, when the row is processed, then the database `CSZ` field is updated with those values joined in the same import flow. | |||
| - [x] AC 8: Given a valid workbook row matches an existing jurisdiction by extracted `JCode`, when the row is processed, then `Name`, `Mailing_Address`, `CSZ`, `IMB_Digits`, and `IMB` are updated in the `Jurisdiction` table. | |||
| - [x] AC 9: Given a valid workbook row is processed, when `IMB_Digits` is derived, then `IMB` is regenerated using the existing `GetIMBCodec.EncodeDigits(...)` functionality. | |||
| - [x] AC 10: Given a workbook row has an extracted `JCode` that does not exist in the database, when the row is processed, then a new `Jurisdiction` row is inserted with the parsed/imported values. | |||
| - [x] AC 11: Given the same `JCode` appears more than once in the workbook, when processing completes, then the last valid occurrence wins and duplicates are counted in the final summary. | |||
| - [x] AC 12: Given a row has malformed `Jurisdiction` text that cannot produce a valid `JCode`, when the row is processed, then that row is counted as failed or invalid, the import continues with subsequent rows, and the final output includes the full imported record details. | |||
| - [x] AC 13: Given the user starts an import, when the operation is in progress, then the page shows AJAX-driven progress feedback for the current import token. | |||
| - [x] AC 14: Given true live row-by-row progress is not technically reliable under Classic ASP/IIS, when the import runs, then the user still sees checkpoint-based progress states instead of a frozen screen. | |||
| - [x] AC 15: Given the import completes, when final results are shown, then the user sees total rows seen, rows updated, rows inserted, duplicates encountered, invalid rows skipped, and failures. | |||
| - [x] AC 16: Given any malformed `Jurisdiction` or `IMB_Digits` build failures occur, when results are shown, then the user can see row numbers, failure reasons, and the full source record for those rows. | |||
| ## Additional Context | |||
| ### Dependencies | |||
| - Windows/IIS runtime with Classic ASP enabled | |||
| - `FreeASPUpload` remains the upload mechanism unless implementation constraints force a localized alternative | |||
| - `Microsoft.ACE.OLEDB.12.0` must be installed and available on the target server for workbook access | |||
| - Existing `GetIMBCodec.EncodeDigits(...)` functionality must remain available during import processing | |||
| - Existing MVC JSON helper in `/workspace/MVC/lib.json.asp` can support polling responses | |||
| ### Testing Strategy | |||
| - Manual IIS test: upload a valid copy of [BRM Permit Info February '26 updated.xlsx](/workspace/Data/BRM Permit%20Info%20February%20'26%20updated.xlsx) and verify updated jurisdiction rows in the database | |||
| - Manual IIS test: upload a non-XLSX file and verify validation failure | |||
| - Manual IIS test: upload a workbook with one required header removed and verify fast-fail header validation | |||
| - Manual IIS test: upload a workbook containing malformed `Jurisdiction` values and verify row-level error reporting with continued processing and full source-record details | |||
| - Manual IIS test: upload a workbook containing missing `JCode` rows and verify the rows are inserted into `Jurisdiction` | |||
| - Manual IIS test: upload a workbook containing invalid `Mailer ID Option 1` or `ZIP + 4` values and verify the full source record is shown in the error output | |||
| - Manual IIS test: upload a workbook containing duplicate `JCode` rows and verify the last valid row wins | |||
| - Manual IIS test: verify progress bar/status polling updates during import and resolves to a final summary state | |||
| - Optional helper-level verification: isolate and test any extracted parsing/normalization helper routines if they are moved out of the controller into reusable VBScript functions | |||
| ### Notes | |||
| - Highest implementation risk is the interaction between Classic ASP request lifecycle, Session locking, and AJAX polling; the implementation should prefer a reliable bounded progress model over an unreliable “live” illusion. | |||
| - ACE/OLEDB availability is a deployment/runtime dependency, not just a coding detail; missing-provider handling must be first-class. | |||
| - Keep the change bounded to the existing jurisdiction import workflow and avoid turning this into a generalized spreadsheet-import framework. | |||
| - If the controller becomes too large, extraction into small local helper functions is acceptable, but avoid broad architectural refactors. | |||
| @@ -0,0 +1,207 @@ | |||
| # Sprint Change Proposal | |||
| **Date:** 2026-03-13 | |||
| **Feature:** XLSX Jurisdiction Import | |||
| **Change Type:** Correct Course during implementation | |||
| **Scope Classification:** Minor | |||
| ## 1. Issue Summary | |||
| The XLSX jurisdiction import feature was implemented from the original quick spec, but the business rules changed during implementation review. | |||
| New required behavior: | |||
| - malformed `Jurisdiction` rows must show the full imported record, not just row number and reason | |||
| - unmatched `JCode` rows must now be inserted into `Jurisdiction` instead of being skipped | |||
| - `IMB_Digits` build failures must also show the full imported record | |||
| Context: | |||
| - the current quick spec explicitly says unmatched `JCode` rows should be skipped | |||
| - the current implementation summary/error reporting is too terse for operators to identify the bad row without manually reopening the workbook | |||
| Evidence: | |||
| - current spec says unmatched `JCode` rows are skipped | |||
| - current implementation logs row-level errors as short messages only | |||
| - stakeholder clarified the corrected behavior after implementation had already started | |||
| ## 2. Impact Analysis | |||
| ### Epic Impact | |||
| No epics or stories artifacts exist for this quick-flow change, so there is no epic backlog to rewrite. | |||
| ### Story Impact | |||
| No formal story artifacts exist. The impact is limited to the current quick-spec-driven implementation. | |||
| ### Artifact Conflicts | |||
| Affected artifacts: | |||
| - `/workspace/_bmad-output/implementation-artifacts/tech-spec-add-xlsx-jurisdiction-import.md` | |||
| - `/workspace/App/Controllers/Jurisdiction/JurisdictionController.asp` | |||
| - `/workspace/App/Views/Jurisdiction/import.asp` | |||
| - `/workspace/App/DomainModels/JurisdictionRepository.asp` if insert support needs refinement | |||
| Conflicts discovered: | |||
| - current spec says unmatched `JCode` rows should be skipped, but new requirement says they must be inserted | |||
| - current spec requires row number and failure reason, but new requirement also needs full row content displayed for specific failures | |||
| ### Technical Impact | |||
| - controller logic must change from update-or-skip to update-or-insert | |||
| - import summary counters must distinguish `updated` and `inserted` | |||
| - row error formatting must include the full workbook row payload for: | |||
| - malformed `Jurisdiction` | |||
| - IMB digit build failure | |||
| - UI summary labels must align with the new counters | |||
| - acceptance criteria and task tracking in the quick spec must be corrected | |||
| ## 3. Recommended Approach | |||
| ### Chosen Path | |||
| **Direct Adjustment** | |||
| ### Rationale | |||
| This is a bounded feature correction, not a broad replanning event. The existing implementation is already concentrated in one controller and one view, so the safest path is to update the quick spec and then amend the implementation directly. | |||
| ### Effort / Risk | |||
| - **Effort:** Low to Medium | |||
| - **Risk:** Medium | |||
| Primary risk areas: | |||
| - insert path must not create malformed or partial `Jurisdiction` records | |||
| - operator-facing error rendering must remain readable when full row payloads are shown | |||
| - summary counters and wording must stay consistent with the new behavior | |||
| ### Timeline Impact | |||
| Small extension to the current implementation pass. No broader program impact identified. | |||
| ## 4. Detailed Change Proposals | |||
| ### A. Quick Spec Updates | |||
| #### A1. Unmatched `JCode` behavior | |||
| **OLD** | |||
| - Rows with extracted `JCode` values not found in `Jurisdiction` should be skipped and counted in the final summary instead of inserted. | |||
| - Acceptance criteria describe unmatched rows as skipped with no insert. | |||
| **NEW** | |||
| - Rows with extracted `JCode` values not found in `Jurisdiction` should be inserted into `Jurisdiction`. | |||
| - Final summary should track inserted rows separately from updated rows. | |||
| **Rationale** | |||
| This aligns the artifact with the corrected business rule. | |||
| #### A2. Full-record row errors | |||
| **OLD** | |||
| - Final result exposes row number and failure reason. | |||
| **NEW** | |||
| - Final result exposes row number, failure reason, and the full imported record for failure cases where operator review is needed. | |||
| **Rationale** | |||
| Operators need enough detail to identify and repair bad workbook rows quickly. | |||
| #### A3. IMB digit failure reporting | |||
| **OLD** | |||
| - `IMB_Digits` failures are treated as row-level failures with summary text. | |||
| **NEW** | |||
| - `IMB_Digits` failures must include the full row content in the error output. | |||
| **Rationale** | |||
| The source data causing the failure must be visible without cross-referencing the spreadsheet manually. | |||
| ### B. Implementation Updates | |||
| #### B1. Insert-on-missing `JCode` | |||
| **OLD** | |||
| - `FindByJCode` failure increments unmatched count and records an error. | |||
| **NEW** | |||
| - `FindByJCode` failure creates a new jurisdiction model and inserts it using the imported values. | |||
| - Import summary tracks `insertedCount`. | |||
| **Rationale** | |||
| This is the main behavioral correction. | |||
| #### B2. Full-row error formatting | |||
| **OLD** | |||
| - Example: `Row 14: Jurisdiction field is missing a code in parentheses.` | |||
| **NEW** | |||
| - Example: `Row 14: Jurisdiction field is missing a code in parentheses. Record: County=..., Jurisdiction=..., Mailing Address=..., City & Township=..., ZIP + 4=..., Mailer ID Option 1=...` | |||
| **Rationale** | |||
| This directly satisfies the new operational requirement. | |||
| #### B3. UI counter updates | |||
| **OLD** | |||
| - `updated`, `unmatched`, `invalid`, `failed`, `duplicates` | |||
| **NEW** | |||
| - `updated`, `inserted`, `invalid`, `failed`, `duplicates` | |||
| **Rationale** | |||
| The UI should reflect what the import actually does. | |||
| ## 5. Implementation Handoff | |||
| ### Scope | |||
| **Minor** | |||
| ### Handoff Recipients | |||
| - Development implementation pass | |||
| - Optional technical writer/doc refresh after code is aligned | |||
| ### Responsibilities | |||
| - Update quick spec to reflect corrected behavior | |||
| - Amend controller logic to insert on missing `JCode` | |||
| - Add full-record context to specified error cases | |||
| - Update import UI summary wording/counters | |||
| - Verify with manual IIS test using the sample workbook and crafted failure rows | |||
| ### Success Criteria | |||
| - malformed `Jurisdiction` rows display full record details | |||
| - missing `JCode` rows insert new jurisdictions successfully | |||
| - IMB digit failures display full record details | |||
| - import summary distinguishes updated vs inserted | |||
| - quick spec reflects final corrected behavior and task state | |||
| @@ -0,0 +1,90 @@ | |||
| --- | |||
| project_name: 'workspace' | |||
| user_name: 'Daniel Covington' | |||
| date: '2026-03-13' | |||
| sections_completed: ['technology_stack', 'language_rules', 'framework_rules', 'testing_rules', 'quality_rules', 'workflow_rules', 'anti_patterns'] | |||
| status: 'complete' | |||
| rule_count: 23 | |||
| optimized_for_llm: true | |||
| --- | |||
| # Project Context for AI Agents | |||
| _This file contains critical rules and patterns that AI agents must follow when implementing code in this project. Focus on unobvious details that agents might otherwise miss._ | |||
| --- | |||
| ## Technology Stack & Versions | |||
| - Runtime: IIS + Classic ASP on Windows | |||
| - Language: VBScript / `.asp` | |||
| - Framework: custom MVC helpers under `MVC/` | |||
| - Data access: ADO via `App/DAL/lib.DAL.asp` | |||
| - Database: Access MDB via Jet/ACE providers | |||
| - Reporting/export: ReportMan, Debenu PDF Library, Chilkat COM components | |||
| - Testing: ASPUnit served through IIS | |||
| - Automation: VBScript and PowerShell scripts outside the web app | |||
| ## Critical Implementation Rules | |||
| ### Language-Specific Rules | |||
| - Start application `.asp` and `.vbs` files with `Option Explicit`. | |||
| - Keep `<!--#include file="..."-->` directives near the top; include order matters because the app relies on side-effect loading. | |||
| - Follow the existing singleton-style factory pattern for repositories instead of introducing new instantiation conventions mid-module. | |||
| - Match the nearby VBScript style exactly; there is no formatter protecting consistency. | |||
| ### Framework-Specific Rules | |||
| - Web entry starts at `index.asp`, but the real bootstrap is `App/include_all.asp`; changes that depend on shared libraries should be wired there only if they are globally required. | |||
| - Routes are initialized by `Routes.Initialize "/App/"`; avoid changing route roots casually because controller links assume that base. | |||
| - Controllers typically orchestrate directly and include views inline; do not introduce partial modern abstractions unless they clearly reduce duplication. | |||
| - Use existing MVC helpers such as `Automapper`, `Flash`, `FormCache`, and `HTMLSecurity` before inventing parallel helpers. | |||
| ### Testing Rules | |||
| - Existing automated coverage is strongest for helper libraries, not end-to-end operational workflows. | |||
| - Any change in controller export/proof logic requires manual verification on Windows/IIS even if tests pass. | |||
| - Run `Tests/Test_All.asp` through IIS for regression checks when changing shared MVC helpers or utility behavior. | |||
| ### Code Quality & Style Rules | |||
| - Preserve PascalCase class and file naming used across controllers, repositories, and view models. | |||
| - Keep repository SQL in the repository layer; avoid scattering new raw SQL into views. | |||
| - When changing a screen, trace controller, repository, view model, and view together because behavior is distributed across those layers. | |||
| - Prefer minimal, local edits over broad refactors; this codebase is highly environment-coupled. | |||
| ### Development Workflow Rules | |||
| - Check `App/app.config.asp` before assuming path behavior; `dev` mode changes export locations and runtime assumptions. | |||
| - Search both `App/` and `ImportService/` when changing statuses, file names, export behavior, or schema-related fields. | |||
| - For schema changes, add a numbered migration under `Data/Migrations/` and then update the affected repository/model code. | |||
| - Treat report templates in `Data/*.rep` as part of the runtime contract, not incidental assets. | |||
| ### Critical Don't-Miss Rules | |||
| - Do not commit new secrets, unlock keys, credentials, or machine-specific paths. Existing hard-coded values are legacy constraints, not a pattern to extend. | |||
| - Do not assume Linux validation is enough. COM, IIS, Jet/ACE, report generation, and network shares are Windows-only concerns here. | |||
| - Do not rename or normalize status strings without global search; automation scripts appear to branch on exact textual statuses. | |||
| - Do not change export filenames or folder structures without auditing downstream consumers such as SnailWorks and import-service scripts. | |||
| - Do not move shared includes or bootstrap files casually; many files rely on implicit availability of globals and helpers. | |||
| - Be careful with view file names: there are similarly named templates such as `CreateKit.asp` and `CreateTrackingKit.asp`, and controllers include them directly. | |||
| --- | |||
| ## Usage Guidelines | |||
| **For AI Agents:** | |||
| - Read this file before implementing any code. | |||
| - Prefer the existing Classic ASP patterns over introducing modern architecture unless explicitly requested. | |||
| - When unsure, choose the option that minimizes environment risk and preserves current operational behavior. | |||
| - Update this file if you discover a repeated brownfield rule that would prevent future mistakes. | |||
| **For Humans:** | |||
| - Keep this file lean and focused on non-obvious implementation constraints. | |||
| - Update it when environment behavior, schema patterns, or core workflow conventions change. | |||
| - Re-check it before delegating larger brownfield changes to an AI agent. | |||
| Last Updated: 2026-03-13 | |||
| @@ -1,3 +0,0 @@ | |||
| mkdir .\volumes\codex_home | |||
| docker compose up -d --build | |||
| docker compose exec codex bash | |||
| @@ -1,10 +1,9 @@ | |||
| services: | |||
| devcontainer: | |||
| bonsai: | |||
| build: . | |||
| container_name: tracking-dev | |||
| container_name: ai-bmad-tracking-kits | |||
| volumes: | |||
| - .:/workspace | |||
| working_dir: /workspace | |||
| stdin_open: true | |||
| tty: true | |||
| - /var/run/docker.sock:/var/run/docker.sock | |||
| ports: | |||
| - "7681:7681" | |||
| @@ -0,0 +1,107 @@ | |||
| # tracking_kits - Route and Integration Contracts | |||
| **Date:** 2026-03-13 | |||
| ## Important Note | |||
| This project does not expose a conventional JSON REST API. Its primary contract surface is a set of Classic ASP controller routes that render HTML pages and process form submissions. In addition, it has file-based export/import interfaces used by batch automation. | |||
| ## Web Route Surface | |||
| Routing is initialized by [../App/app.config.asp](../App/app.config.asp), and requests are dispatched from controller ASP files under `App/Controllers`. | |||
| ### HomeController | |||
| - `Index` | |||
| - Purpose: render the switchboard/home screen | |||
| - `CreateKit` | |||
| - Purpose: list jurisdictions with pagination for kit creation | |||
| - `Search` | |||
| - Purpose: search jurisdictions by `JCode`, `Name`, address fields, and IMB values | |||
| - `Print` | |||
| - Purpose: render a sample/report PDF using `Label_Report.rep` | |||
| ### KitController | |||
| - `Index` | |||
| - Purpose: paged list of kits | |||
| - `Search` | |||
| - Purpose: search kits by `ID`, `JobNumber`, and `JCode` | |||
| - `SwitchBoardIndex` | |||
| - Purpose: operational list for label-based kits | |||
| - `SwitchBoardEdit` | |||
| - Purpose: edit/view a single kit switchboard entry | |||
| - `SwitchBoardPurpleEnvelopsIndex` | |||
| - Purpose: list purple-envelope kits | |||
| - `SwitchBoardPurpleEnvelopeEdit` | |||
| - Purpose: edit purple-envelope kit details, color options, and precinct color data | |||
| - `SwitchBoardPurpleEnvelopeEditPost` | |||
| - Purpose: update purple-envelope form fields and status | |||
| - `AssignKitColorPost` | |||
| - Purpose: assign a color to all labels/records in a kit | |||
| - `AssignPrecinctColorsPost` | |||
| - Purpose: assign colors at the precinct level | |||
| - `ExportTrackingLabels(id)` | |||
| - Purpose: generate and move a kit label PDF | |||
| - `ExportSnailWorksTracking(id)` | |||
| - Purpose: generate SnailWorks CSV export data | |||
| ### Other Controllers | |||
| The repository structure implies standard CRUD routes for: | |||
| - `JurisdictionController` | |||
| - `ContactsController` | |||
| - `InkjetRecordsController` | |||
| - `SettingsController` | |||
| - `CustomOfficeCopyJobController` | |||
| - `KitLabelsController` | |||
| These are primarily view-rendering and form-post endpoints. | |||
| ## Form / POST Contracts | |||
| Observed POST patterns: | |||
| - Classic HTML forms | |||
| - anti-CSRF tokens managed by `HTMLSecurity.SetAntiCSRFToken` | |||
| - redirect-on-success flow with flash messaging | |||
| - model hydration via `Automapper.AutoMap(Request.Form, model)` | |||
| ## File-Based Integration Contracts | |||
| ### Tracking Label PDF Export | |||
| - Triggered from kit workflows | |||
| - Uses `Data/Label_Report.rep` | |||
| - Writes PDF output to app-local `Data/` temporarily | |||
| - Moves final artifact into `ExportDirectory\<Jurisdiction>\...` | |||
| ### SnailWorks Export | |||
| - Generates CSV files per kit | |||
| - Includes a header section plus detail records | |||
| - Output path depends on configured `ExportDirectory` | |||
| ### Inkjet Export / Proof / Import Processing | |||
| - `ImportService/TrackingDataImport.vbs` performs status-driven processing | |||
| - Outputs CSV and PDF artifacts | |||
| - Reads and writes from configured directories and network shares | |||
| - Uses COM-based CSV/report/SFTP functionality | |||
| ## Security and Validation Notes | |||
| - CSRF checks are present for important form posts. | |||
| - Input validation exists unevenly; some validation calls are commented out in controllers. | |||
| - There is no API schema enforcement layer; the contract is defined by forms, SQL expectations, and downstream file formats. | |||
| ## Change Guidance | |||
| - Treat controller action names, form field names, status strings, and export file names as part of the operational contract. | |||
| - Before changing an export format, inspect both web controllers and import-service scripts. | |||
| - Before changing a status value, search for every status-dependent branch in both `App/` and `ImportService/`. | |||
| --- | |||
| _Generated for brownfield analysis._ | |||
| @@ -0,0 +1,131 @@ | |||
| # tracking_kits - Architecture | |||
| **Date:** 2026-03-13 | |||
| **Application Type:** Single-part Classic ASP web application | |||
| ## Executive Summary | |||
| `tracking_kits` is a server-rendered IIS application that manages operational workflows around election mail pieces. It combines browser-driven CRUD screens with file-system side effects and batch automation. The architecture is straightforward but environment-sensitive: controllers render views and call repositories, while proofs, inkjet exports, and tracking exports rely on COM libraries, report templates, and Windows file shares. | |||
| ## Runtime Flow | |||
| 1. IIS serves [../index.asp](../index.asp). | |||
| 2. `index.asp` redirects to [../App/Controllers/Home/HomeController.asp](../App/Controllers/Home/HomeController.asp). | |||
| 3. The controller includes [../App/include_all.asp](../App/include_all.asp), which loads the MVC framework, DAL, app config, and all repositories. | |||
| 4. `Routes.Initialize "/App/"` from [../App/app.config.asp](../App/app.config.asp) establishes route resolution for controller files under `App/`. | |||
| 5. Controller actions populate view-model objects and include `.asp` view templates. | |||
| 6. Repositories issue SQL through `DAL.Query`, `DAL.Execute`, and `DAL.PagedQuery`. | |||
| 7. Some controller actions and automation scripts create files, call report engines, and move outputs into export directories. | |||
| ## Major Architectural Pieces | |||
| ### Presentation Layer | |||
| - Controller classes live under `App/Controllers/*`. | |||
| - Views live under `App/Views/*`. | |||
| - View-model types live under `App/ViewModels/*`. | |||
| - Shared layout fragments live in `App/Views/Shared/`. | |||
| This layer is synchronous and page-oriented. There is no client-side SPA architecture in the codebase. | |||
| ### Application/Workflow Layer | |||
| Controller actions contain most orchestration logic. Key examples: | |||
| - `HomeController.CreateKit` and `HomeController.Search` drive jurisdiction lookup for kit creation. | |||
| - `KitController.SwitchBoardIndex` and `SwitchBoardPurpleEnvelopsIndex` drive operational dashboards. | |||
| - `KitController.ExportTrackingLabels` renders a report to PDF and moves it into an export folder. | |||
| - `KitController.ExportSnailWorksTracking` writes CSV exports from repository-provided data. | |||
| - Purple-envelope color assignment logic is handled directly in controller POST actions. | |||
| ### Persistence Layer | |||
| - ADO access is centralized in `App/DAL/lib.DAL.asp`. | |||
| - Repositories under `App/DomainModels/` contain raw SQL and map records into model classes. | |||
| - Pagination and search are implemented inside repositories rather than a service layer. | |||
| - The database appears to be an Access MDB with migrations tracked in `Data/Migrations`. | |||
| ### Shared Framework Layer | |||
| The `MVC/` directory provides reusable infrastructure: | |||
| - `lib.MVC.asp` and `lib.Routes.asp` for dispatch/routing | |||
| - `lib.Automapper.asp` for record/form mapping | |||
| - `lib.HTMLSecurity.asp` for anti-CSRF helpers | |||
| - `lib.Flash.asp`, `lib.FormCache.asp`, `lib.Helpers.asp`, `lib.Collections.asp`, and related helpers | |||
| This shared code is effectively the application's internal framework. | |||
| ### Batch / Integration Layer | |||
| - `ImportService/TrackingDataImport.vbs` handles import/export/proof workflows outside HTTP requests. | |||
| - PowerShell and VBScript utilities exist for address updates, service installation, and batch processing. | |||
| - COM libraries provide CSV, SFTP, report, and PDF functionality. | |||
| ## Data Architecture | |||
| The schema is migration-driven. Core tables inferred from migrations and repositories: | |||
| - `Jurisdiction` | |||
| - `Contact` | |||
| - `Settings` | |||
| - `Kit` | |||
| - `Kit_Labels` / `KitLabels` | |||
| - `InkJetRecords` | |||
| - `CustomOfficeCopyJob` | |||
| - `Colors` | |||
| Relationships are implied through foreign-key migrations and repository joins, especially around kits, labels, inkjet records, contacts, and jurisdictions. | |||
| ## Route / Action Design | |||
| This app exposes controller-action routes rather than a versioned API. The practical contract surface is: | |||
| - User-facing pages under `Home`, `Kit`, `Jurisdiction`, `Contacts`, `Settings`, `InkjetRecords`, and `CustomOfficeCopyJob` | |||
| - Form POST actions guarded by anti-CSRF tokens | |||
| - Operational actions that change status and produce files | |||
| There is no evidence of JSON APIs being a primary integration mechanism. | |||
| ## External Dependencies and Constraints | |||
| ### Windows / IIS | |||
| - Runtime assumes IIS + Classic ASP. | |||
| - Paths in config and scripts reference drive letters and UNC shares. | |||
| - End-to-end behavior depends on Windows COM registration. | |||
| ### COM / ActiveX | |||
| - ReportMan for report rendering | |||
| - Debenu PDF Library for PDF operations | |||
| - Chilkat libraries for unlockable COM features and SFTP/CSV support | |||
| ### File-System Coupling | |||
| - Export folders are created on demand. | |||
| - Existing files may be deleted and replaced. | |||
| - Generated artifacts move between local app paths and shared export directories. | |||
| ## Security and Operational Risks | |||
| - Environment-specific values and secrets are embedded in code/config scripts today. | |||
| - File-system writes and network share access are central to the workflow and are hard to test in isolation. | |||
| - Business logic is distributed across controllers and external scripts, so changes often require cross-file tracing. | |||
| ## Testing Strategy | |||
| - Helper/framework tests exist through ASPUnit. | |||
| - There is little evidence of automated coverage for end-to-end kit/proof/export workflows. | |||
| - High-confidence changes in controller/export logic will require manual IIS-based verification on Windows. | |||
| ## Change Guidance | |||
| - For UI flow changes, inspect the controller, view model, view, and repository together. | |||
| - For export/proof changes, inspect both web controllers and `ImportService/TrackingDataImport.vbs`. | |||
| - For schema changes, add a migration under `Data/Migrations` and update the affected repository/model classes. | |||
| - For environment changes, audit `App/app.config.asp`, import scripts, and any hard-coded paths or COM usage. | |||
| --- | |||
| _Generated for brownfield analysis._ | |||
| @@ -0,0 +1,138 @@ | |||
| # tracking_kits - Component Inventory | |||
| **Date:** 2026-03-13 | |||
| ## Controllers | |||
| ### Home | |||
| - [../App/Controllers/Home/HomeController.asp](../App/Controllers/Home/HomeController.asp) | |||
| - Responsibilities: | |||
| - Render the switchboard | |||
| - Search jurisdictions | |||
| - Start the create-kit flow | |||
| - Demo PDF/report generation | |||
| ### Kit | |||
| - [../App/Controllers/Kit/KitController.asp](../App/Controllers/Kit/KitController.asp) | |||
| - Responsibilities: | |||
| - Kit switchboard pages | |||
| - Purple-envelope operational screens | |||
| - Kit edit/update flows | |||
| - Label PDF export | |||
| - SnailWorks export generation | |||
| - Color assignment and related POST actions | |||
| ### Other Domain Controllers | |||
| - `JurisdictionController` | |||
| - `InkjetRecordsController` | |||
| - `ContactsController` | |||
| - `SettingsController` | |||
| - `CustomOfficeCopyJobController` | |||
| - `KitLabelsController` | |||
| These appear to follow the same CRUD-oriented MVC pattern used elsewhere in the app. | |||
| ## Repository Components | |||
| ### Core Repositories | |||
| - `JurisdictionRepository` | |||
| - `KitRepository` | |||
| - `KitLabelsRepository` | |||
| - `InkjetRecordsRepository` | |||
| - `SettingsRepository` | |||
| - `ContactsRepository` | |||
| - `SnailWorksRepository` | |||
| - `CustomOfficeCopyJobRepository` | |||
| - `ColorsRepository` | |||
| ### Shared Characteristics | |||
| - Model classes and repository classes live in the same file. | |||
| - SQL is embedded directly in repository methods. | |||
| - Pagination, search, and mapping are implemented per repository. | |||
| - Repositories are exposed through singleton-style factory functions. | |||
| ## View Models | |||
| View models under `App/ViewModels/` shape data for server-side rendering. Notable classes are used for: | |||
| - paged index screens | |||
| - switchboard screens | |||
| - per-domain CRUD forms | |||
| - purple envelope and inkjet workflows | |||
| ## UI Surfaces | |||
| ### Shared Layout | |||
| - [../App/Views/Shared/layout.header.asp](../App/Views/Shared/layout.header.asp) | |||
| - [../App/Views/Shared/layout.footer.asp](../App/Views/Shared/layout.footer.asp) | |||
| ### Home Screens | |||
| - switchboard index | |||
| - create kit | |||
| - create tracking kit | |||
| ### Kit Screens | |||
| - kit CRUD pages | |||
| - switchboard list/edit pages | |||
| - purple envelope switchboard list/edit pages | |||
| ### Other Screens | |||
| - jurisdiction CRUD/import pages | |||
| - contacts CRUD pages | |||
| - inkjet record CRUD pages | |||
| - settings CRUD pages | |||
| - custom office copy job pages | |||
| ## Shared Framework Components | |||
| The `MVC/` directory provides reusable infrastructure rather than business-domain components: | |||
| - routing and dispatch | |||
| - automapper | |||
| - HTML helpers | |||
| - anti-CSRF/security helpers | |||
| - flash messages and form cache | |||
| - collections and enumerable helpers | |||
| - upload, strings, validations, JSON support | |||
| ## Automation / Batch Components | |||
| ### Import Service | |||
| - [../ImportService/TrackingDataImport.vbs](../ImportService/TrackingDataImport.vbs) | |||
| - Handles polling/status-driven operations outside the web request lifecycle. | |||
| ### Supporting Scripts | |||
| - `InstallService.vbs` | |||
| - `Data/Update_Addresses.ps1` | |||
| - `App/ScaffoldRepo.vbs` | |||
| ## Reporting Assets | |||
| - `Data/Label_Report.rep` | |||
| - `Data/Proofs.rep` | |||
| - `Data/Office-Copy-Proof.rep` | |||
| - `Data/Custom Office Copies Proof.rep` | |||
| These files are operational assets, not just documentation or samples. Controller and script behavior depends on them. | |||
| ## Reuse Guidance | |||
| - New CRUD-style features should follow the existing controller/view/repository/view-model grouping pattern. | |||
| - Reuse shared layout includes and MVC helpers instead of inventing alternate rendering patterns. | |||
| - Reuse repository paging/search conventions if extending list screens. | |||
| - Reuse migration numbering conventions for schema changes. | |||
| --- | |||
| _Generated for brownfield analysis._ | |||
| @@ -0,0 +1,132 @@ | |||
| # tracking_kits - Data Models | |||
| **Date:** 2026-03-13 | |||
| ## Database Overview | |||
| The project uses an Access database accessed through ADO. The authoritative schema history lives in [../Data/Migrations](../Data/Migrations). Repository SQL and migration names indicate a relational model centered on jurisdictions, kits, labels, contacts, settings, inkjet records, and custom office copy jobs. | |||
| ## Core Tables | |||
| ### Jurisdiction | |||
| Primary fields observed in `JurisdictionRepository`: | |||
| - `JCode` | |||
| - `Name` | |||
| - `Mailing_Address` | |||
| - `CSZ` | |||
| - `IMB` | |||
| - `IMB_Digits` | |||
| Later migrations indicate additional fields such as `Title`. | |||
| **Role:** Master data for election jurisdictions and mailing identity information. | |||
| ### Contact | |||
| Created in migration 02 and joined from import/export logic. | |||
| **Role:** Contact and addressing data associated with a jurisdiction. | |||
| ### Settings | |||
| Created in migration 03 and used by `SettingsRepository`. | |||
| **Role:** Application configuration stored in the database, including STID-related values referenced by kit workflows. | |||
| ### Kit | |||
| Observed fields in `KitRepository` and migrations: | |||
| - `ID` | |||
| - `JobNumber` | |||
| - `JCode` | |||
| - `CreatedOn` | |||
| - `LabelsPrinted` | |||
| - `ExportedToSnailWorks` | |||
| - `InkJetJob` | |||
| - `JobType` | |||
| - `Filename` | |||
| - `Cass` | |||
| - `Status` | |||
| - `OutboundSTID` | |||
| - `InboundSTID` | |||
| - `OfficeCopiesAmount` | |||
| **Role:** Central workflow entity representing a print/export job for a jurisdiction. | |||
| ### KitLabels | |||
| Created in migration 05 and counted in kit switchboard queries. | |||
| **Role:** Individual label records associated with a kit. | |||
| ### InkJetRecords | |||
| Created in migration 09 and extended later with `KitLabelID` and `ColorId`. | |||
| **Role:** Inkjet output records tied to kits and, later, colors and label relationships. | |||
| ### CustomOfficeCopyJob | |||
| Created in migration 18. | |||
| **Role:** Tracks custom office copy proof/export work outside the standard label path. | |||
| ### Colors | |||
| Created in migration 19. | |||
| **Role:** Lookup table used for purple-envelope color assignment and inkjet record coloring. | |||
| ## Relationship Summary | |||
| - `Kit.JCode` links kits to a jurisdiction. | |||
| - `KitLabels.KitId` links labels to a kit. | |||
| - `InkJetRecords.KitId` links inkjet records to a kit. | |||
| - Later migrations add relationship and foreign-key support between key tables. | |||
| - Contacts appear tied to jurisdictions through a jurisdiction code field. | |||
| - Custom office copy jobs appear tied to jurisdiction and batch-processing state. | |||
| ## Migration History | |||
| Notable schema evolution based on migration names: | |||
| 1. Create `Jurisdiction` | |||
| 2. Create `Contact` | |||
| 3. Create `Settings` | |||
| 4. Create `Kit` | |||
| 5. Create `Kit_Labels` | |||
| 6. Alter `Kit` | |||
| 7. Alter `Kit` labels/settings behavior | |||
| 8. Add inkjet job to `Kit` | |||
| 9. Create `InkJetRecords` | |||
| 10. Add table relations | |||
| 11. Add foreign keys | |||
| 12. Add type to `Kit` | |||
| 13. Add file/cass fields to `Kit` | |||
| 14. Add STIDs to `Kit` | |||
| 15. Add `KitLabelID` to `InkJetRecords` | |||
| 16. Add title to `Jurisdiction` | |||
| 17. Add office copies amount to `Kit` | |||
| 18. Create `CustomOfficeCopyJob` | |||
| 19. Create `Colors` | |||
| 20. Add `ColorId` to `InkJetRecords` | |||
| ## Data Access Patterns | |||
| - Repositories use direct SQL strings and `Automapper.AutoMap`. | |||
| - Search methods build `LIKE` queries from controller-provided values. | |||
| - Pagination relies on `DAL.PagedQuery`. | |||
| - Validation is light and often commented out in controller flows. | |||
| ## Change Guidance | |||
| - Treat migrations as the source of truth for schema evolution. | |||
| - If a repository query changes shape, verify all views and export scripts using that entity. | |||
| - Search both `App/` and `ImportService/` before renaming fields or changing status semantics. | |||
| --- | |||
| _Generated for brownfield analysis._ | |||
| @@ -0,0 +1,81 @@ | |||
| # tracking_kits - Development Guide | |||
| **Date:** 2026-03-13 | |||
| ## Prerequisites | |||
| - Windows host with IIS | |||
| - Classic ASP enabled in IIS | |||
| - Access/Jet or ACE provider available | |||
| - COM dependencies installed if you need to exercise print/proof/export features | |||
| - Access to required local drives or network shares for non-dev environments | |||
| ## Local Setup | |||
| 1. Configure an IIS site whose root points to the repository root. | |||
| 2. Confirm `index.asp` is configured as the default document. The repo `web.config` already adds it. | |||
| 3. Enable Classic ASP in IIS. | |||
| 4. Review [../App/app.config.asp](../App/app.config.asp): | |||
| - `Routes.Initialize "/App/"` | |||
| - `dev` flag selection | |||
| - `ExportDirectory` mapping | |||
| - external CSS/JS URLs | |||
| 5. If you need export/proof behavior, ensure the dependencies in [../Dependancies](../Dependancies) are present and registered appropriately on the machine. | |||
| ## Running the Application | |||
| - Browse the IIS site root. | |||
| - The browser is redirected from `index.asp` into the Home controller. | |||
| - Use the switchboard UI to access kit creation, tracking kits, purple envelope jobs, and custom office copy jobs. | |||
| ## Running Tests | |||
| - Open [../Tests/Test_All.asp](../Tests/Test_All.asp) through IIS. | |||
| - Expect coverage to focus on helper libraries and low-level utilities rather than full application workflows. | |||
| ## Working with the Database | |||
| - The app uses repositories and ADO, not an ORM. | |||
| - Sample MDB files exist in `Data/`. | |||
| - Schema evolution is implemented in [../Data/Migrations](../Data/Migrations). | |||
| - Before changing a repository query, inspect the corresponding migration history and view usage. | |||
| ## Working with Reports and Exports | |||
| - Report definitions live in `Data/*.rep`. | |||
| - PDF/export code appears in `KitController` and import-service scripts. | |||
| - Many of these flows depend on file-system writes and COM objects, so static edits should be followed by manual Windows verification. | |||
| ## Common Brownfield Change Workflow | |||
| 1. Identify the entry controller action and matching view. | |||
| 2. Trace the repository calls and any related view-model class. | |||
| 3. Check whether the same business rule also exists in `ImportService/`. | |||
| 4. If persistence changes are needed, add a migration and update repository SQL. | |||
| 5. Run helper tests if applicable. | |||
| 6. Manually verify the affected page or export flow in IIS on Windows. | |||
| ## Code Conventions Observed | |||
| - Use `Option Explicit` at the top of ASP/VBScript files. | |||
| - Keep `<!--#include file="..."-->` directives near the top. | |||
| - Follow existing PascalCase naming for classes and descriptive file names. | |||
| - Match the surrounding indentation and spacing style. | |||
| - Avoid introducing new abstractions unless they reduce repeated controller/repository patterns. | |||
| ## Environment Caveats | |||
| - `App/app.config.asp` embeds environment switching and COM unlock behavior. | |||
| - `ImportService/TrackingDataImport.vbs` contains separate path/config handling from the web app. | |||
| - This workspace cannot validate IIS, COM registration, Jet/ACE, or network-share access. | |||
| ## Recommended Validation by Change Type | |||
| - **View-only changes:** Manual browser verification in IIS. | |||
| - **Controller/repository changes:** Browser verification plus targeted test runner pass if helper code changes. | |||
| - **Schema changes:** Migration review, repository validation, and manual smoke test. | |||
| - **Proof/export changes:** Windows-only manual verification of generated files and downstream handoff behavior. | |||
| --- | |||
| _Generated for brownfield analysis._ | |||
| @@ -0,0 +1,72 @@ | |||
| # tracking_kits Documentation Index | |||
| **Type:** monolith | |||
| **Primary Language:** Classic ASP / VBScript | |||
| **Architecture:** MVC-style server-rendered web application with repository-backed data access | |||
| **Last Updated:** 2026-03-13 | |||
| ## Project Overview | |||
| `tracking_kits` is a Windows-hosted Classic ASP application for election mail operations. It helps staff create mail tracking kits, generate proofs, prepare inkjet output files for printers, and export tracking data for downstream mail-tracking systems such as SnailWorks. | |||
| The application is not a modern REST service. It is a server-rendered IIS site that routes requests into controller `.asp` files, renders views from `App/Views`, and accesses an Access/Jet database through repository classes under `App/DomainModels`. | |||
| ## Quick Reference | |||
| - **Entry Point:** [../index.asp](../index.asp) | |||
| - **Primary Bootstrap:** [../App/include_all.asp](../App/include_all.asp) | |||
| - **Route Initialization:** [../App/app.config.asp](../App/app.config.asp) | |||
| - **MVC Framework:** [../MVC](../MVC) | |||
| - **Application Code:** [../App](../App) | |||
| - **Database Migrations:** [../Data/Migrations](../Data/Migrations) | |||
| - **Import/Automation:** [../ImportService](../ImportService) | |||
| - **Tests:** [../Tests](../Tests) | |||
| ## Generated Documentation | |||
| - [Project Overview](./project-overview.md) - Purpose, classification, stack, and major capabilities. | |||
| - [Source Tree Analysis](./source-tree-analysis.md) - Annotated repo layout and critical folders. | |||
| - [Architecture](./architecture.md) - Runtime architecture, request flow, dependencies, and constraints. | |||
| - [Development Guide](./development-guide.md) - Local setup, IIS assumptions, test execution, and change workflow. | |||
| - [Component Inventory](./component-inventory.md) - Controllers, repositories, UI surfaces, shared libraries, and automation components. | |||
| - [Data Models](./data-models.md) - Core tables, relationships, and migration history. | |||
| - [API Contracts](./api-contracts.md) - Route/action surface and non-HTTP export interfaces. | |||
| - [Module Map](./module-map.md) - Feature-by-feature guide to the files you should open first when making changes. | |||
| ## Existing Documentation | |||
| - [README.md](../README.md) - Short project summary and current running notes. | |||
| - [AGENTS.md](../AGENTS.md) - Repository conventions and BMAD skill instructions used in this workspace. | |||
| ## Getting Started | |||
| ### Runtime Prerequisites | |||
| - Windows with IIS and Classic ASP enabled | |||
| - Access/Jet or ACE database provider | |||
| - COM components used by the app registered on the host if you need printing/export features | |||
| - Access to the configured export/import network shares in non-dev environments | |||
| ### Local Development | |||
| 1. Point an IIS site at the repository root. | |||
| 2. Ensure `index.asp` is a default document and Classic ASP is enabled. | |||
| 3. Review [../App/app.config.asp](../App/app.config.asp) for the active `dev` mode and export directory mapping. | |||
| 4. If you need printer/proof/export behavior, verify the COM dependencies under [../Dependancies](../Dependancies) are installed and licensed. | |||
| ### Tests | |||
| - Open [../Tests/Test_All.asp](../Tests/Test_All.asp) through IIS to run the ASPUnit suite. | |||
| - The automated tests primarily cover helper libraries, not the full application workflow. | |||
| ## For Brownfield Changes | |||
| - Read [architecture.md](./architecture.md) first for the request/data flow. | |||
| - Read [module-map.md](./module-map.md) next if you already know which feature area you need to change. | |||
| - Read [data-models.md](./data-models.md) before touching repositories or migrations. | |||
| - Read [development-guide.md](./development-guide.md) before changing environment-sensitive code. | |||
| - Read [_bmad-output/project-context.md](../_bmad-output/project-context.md) before asking an AI agent to implement changes. | |||
| --- | |||
| _Documentation generated for brownfield analysis._ | |||
| @@ -0,0 +1,284 @@ | |||
| # tracking_kits - Module Map | |||
| **Date:** 2026-03-13 | |||
| This guide is the shortest path into the codebase when you need to change a specific feature area. It is organized by module and operational workflow rather than by folder alone. | |||
| ## Fastest Starting Points | |||
| If you need to make a change, start here first: | |||
| - **Kit creation / label workflows:** [../App/Controllers/Kit/KitController.asp](../App/Controllers/Kit/KitController.asp) | |||
| - **Jurisdiction search / import:** [../App/Controllers/Jurisdiction/JurisdictionController.asp](../App/Controllers/Jurisdiction/JurisdictionController.asp) | |||
| - **Proof/export behavior:** [../App/Controllers/Kit/KitController.asp](../App/Controllers/Kit/KitController.asp) and [../ImportService/TrackingDataImport.vbs](../ImportService/TrackingDataImport.vbs) | |||
| - **Inkjet / color assignment:** [../App/Controllers/Kit/KitController.asp](../App/Controllers/Kit/KitController.asp) and [../App/DomainModels/InkjetRecordsRepository.asp](../App/DomainModels/InkjetRecordsRepository.asp) | |||
| - **Database/schema behavior:** [../Data/Migrations](../Data/Migrations) and the matching repository under [../App/DomainModels](../App/DomainModels) | |||
| ## 1. Kit Module | |||
| ### What it owns | |||
| - creating tracking kits | |||
| - creating label records for a kit | |||
| - operational switchboard screens | |||
| - purple-envelope workflows | |||
| - SnailWorks export | |||
| - label PDF export | |||
| - status transitions on kits | |||
| ### Main files | |||
| - [../App/Controllers/Kit/KitController.asp](../App/Controllers/Kit/KitController.asp) | |||
| - [../App/DomainModels/KitRepository.asp](../App/DomainModels/KitRepository.asp) | |||
| - [../App/DomainModels/KitLabelsRepository.asp](../App/DomainModels/KitLabelsRepository.asp) | |||
| - [../App/DomainModels/SnailWorksRepository.asp](../App/DomainModels/SnailWorksRepository.asp) | |||
| - [../App/Views/Kit/CreateTrackingKit.asp](../App/Views/Kit/CreateTrackingKit.asp) | |||
| - [../App/Views/Kit/SwitchBoardIndex.asp](../App/Views/Kit/SwitchBoardIndex.asp) | |||
| - [../App/Views/Kit/SwitchBoardEdit.asp](../App/Views/Kit/SwitchBoardEdit.asp) | |||
| - [../App/Views/Kit/SwitchBoardPurpleEnvelopeEdit.asp](../App/Views/Kit/SwitchBoardPurpleEnvelopeEdit.asp) | |||
| ### Read order | |||
| 1. `KitController.CreateTrackingKit` / `CreatePost` / `CreateTrackingKitPost` | |||
| 2. `KitRepository.AddNew` | |||
| 3. `KitLabelsRepository.BulkAdd` | |||
| 4. `ExportTrackingLabels` and `ExportSnailWorksTracking` | |||
| 5. matching view template | |||
| ### Important coupling | |||
| - `CreatePost` creates a kit, bulk-creates labels, then immediately generates export artifacts. | |||
| - `CreateTrackingKitPost` creates the kit and labels but does not call the export methods. | |||
| - `SwitchBoardEdit` is the main inspection screen for label-based kits. | |||
| - `SwitchBoardPurpleEnvelopeEdit` combines kit data, STID choices, color lists, and precinct-level color data in one page model. | |||
| - Purple-envelope color changes are persisted through `InkjetRecordsRepository`, not `KitRepository`. | |||
| ### Hotspots to treat carefully | |||
| - status strings such as `Ready to Assign Labels` and `Ready To Assign STIDS` | |||
| - export filenames and folder structure | |||
| - any change to `JobType`, `OutboundSTID`, `InboundSTID`, or `OfficeCopiesAmount` | |||
| - label creation count and bulk-add behavior | |||
| ## 2. Jurisdiction Module | |||
| ### What it owns | |||
| - jurisdiction CRUD | |||
| - jurisdiction search | |||
| - jurisdiction-linked contact display | |||
| - jurisdiction file import | |||
| - entry path into kit creation | |||
| ### Main files | |||
| - [../App/Controllers/Jurisdiction/JurisdictionController.asp](../App/Controllers/Jurisdiction/JurisdictionController.asp) | |||
| - [../App/DomainModels/JurisdictionRepository.asp](../App/DomainModels/JurisdictionRepository.asp) | |||
| - [../App/DomainModels/ContactsRepository.asp](../App/DomainModels/ContactsRepository.asp) | |||
| - [../App/Views/Jurisdiction/index.asp](../App/Views/Jurisdiction/index.asp) | |||
| - [../App/Views/Jurisdiction/edit.asp](../App/Views/Jurisdiction/edit.asp) | |||
| - [../App/Views/Jurisdiction/import.asp](../App/Views/Jurisdiction/import.asp) | |||
| - [../App/Views/Home/CreateTrackingKit.asp](../App/Views/Home/CreateTrackingKit.asp) | |||
| ### Read order | |||
| 1. `JurisdictionController.Index` / `Search` | |||
| 2. `JurisdictionRepository.FindPaged` / `SearchTablePaged` | |||
| 3. `JurisdictionController.ImportPost` if the change involves file import | |||
| 4. related edit/create views | |||
| ### Important coupling | |||
| - Search fields are hard-coded in both `HomeController.Search` and `JurisdictionController.Search`. | |||
| - `JurisdictionController.Edit` also loads related contacts. | |||
| - `ImportPost` is one of the denser controller methods in the app: it handles file upload validation, file rewrite, Jet text-driver parsing, SQL projection, and then persistence/import behavior. | |||
| - The kit-creation chooser in the Home area links into `KitController.createTrackingKit` by `JCode`. | |||
| ### Hotspots to treat carefully | |||
| - import file format assumptions: accepted extensions, header handling, tab-vs-delimited mode | |||
| - SQL expressions that derive `JCODE`, `Name`, `CSZ`, and `IMB_Digits` | |||
| - any field rename touching both repository SQL and import mapping | |||
| ## 3. Inkjet / Purple Envelope Module | |||
| ### What it owns | |||
| - inkjet record CRUD | |||
| - precinct-level data for purple-envelope jobs | |||
| - color assignment per kit or precinct | |||
| ### Main files | |||
| - [../App/Controllers/InkjetRecords/InkjetRecordsController.asp](../App/Controllers/InkjetRecords/InkjetRecordsController.asp) | |||
| - [../App/DomainModels/InkjetRecordsRepository.asp](../App/DomainModels/InkjetRecordsRepository.asp) | |||
| - [../App/Views/Kit/SwitchBoardPurpleEnvelopeEdit.asp](../App/Views/Kit/SwitchBoardPurpleEnvelopeEdit.asp) | |||
| - [../App/DomainModels/ColorsRepository.asp](../App/DomainModels/ColorsRepository.asp) | |||
| ### Read order | |||
| 1. `KitController.SwitchBoardPurpleEnvelopeEdit` | |||
| 2. `InkjetRecordsRepository.GetDistinctPrecinctsByKitId` | |||
| 3. `AssignKitColorPost` / `AssignPrecinctColorsPost` | |||
| 4. `InkjetRecordsRepository.UpdateColorForKit` / `UpdateColorForPrecinct` | |||
| ### Important coupling | |||
| - purple-envelope UI reads colors and precinct data live in the view | |||
| - current color names are resolved inside the template using `ColorsRepository.FindByID` | |||
| - the kit edit flow and the precinct color flow share the same screen but use separate forms and anti-CSRF tokens | |||
| ### Hotspots to treat carefully | |||
| - precinct key naming convention: `PrecinctColor_<PRECINCT>` | |||
| - `ColorId` propagation rules | |||
| - query performance if precinct count grows, because the view performs color lookups during rendering | |||
| ## 4. Export / Proof Module | |||
| ### What it owns | |||
| - label PDF generation | |||
| - SnailWorks CSV export | |||
| - report template usage | |||
| - file movement into export directories | |||
| ### Main files | |||
| - [../App/Controllers/Kit/KitController.asp](../App/Controllers/Kit/KitController.asp) | |||
| - [../App/DomainModels/SnailWorksRepository.asp](../App/DomainModels/SnailWorksRepository.asp) | |||
| - [../Data/Label_Report.rep](../Data/Label_Report.rep) | |||
| - [../Data/Proofs.rep](../Data/Proofs.rep) | |||
| - [../Data/Office-Copy-Proof.rep](../Data/Office-Copy-Proof.rep) | |||
| - [../Data/Custom Office Copies Proof.rep](../Data/Custom%20Office%20Copies%20Proof.rep) | |||
| ### Read order | |||
| 1. `ExportTrackingLabels` | |||
| 2. `ExportSnailWorksTracking` | |||
| 3. `SnailWorksRepository.GetSnailWorksExportById` | |||
| 4. report template file referenced by the action | |||
| ### Important coupling | |||
| - export methods depend on `ExportDirectory` from [../App/app.config.asp](../App/app.config.asp) | |||
| - label PDF generation uses a temporary file in `Data\` before moving it to the final folder | |||
| - SnailWorks header values are partially hard-coded inside repository SQL | |||
| - SnailWorks detail records join `InkjetRecords`, `KitLabels`, and `Kit` | |||
| ### Hotspots to treat carefully | |||
| - COM object availability and provider connection strings | |||
| - jurisdiction-based folder naming | |||
| - hard-coded user IDs, emails, and export metadata in `SnailWorksRepository` | |||
| - file overwrite/delete behavior before new export generation | |||
| ## 5. Import Service / Batch Automation Module | |||
| ### What it owns | |||
| - status-driven processing outside the web app | |||
| - custom office copy proof generation | |||
| - export/import batch processing | |||
| - downstream handoff logic such as SFTP and file polling | |||
| ### Main files | |||
| - [../ImportService/TrackingDataImport.vbs](../ImportService/TrackingDataImport.vbs) | |||
| - [../InstallService.vbs](../InstallService.vbs) | |||
| - [../CiCd](../CiCd) | |||
| ### What to know before editing | |||
| - this script is effectively a second application with its own environment config | |||
| - it contains hard-coded path, provider, and external integration behavior separate from `App/app.config.asp` | |||
| - the file is encoded differently from most repo files and is harder to inspect casually | |||
| - status transitions used here overlap with values set in web controllers | |||
| ### Hotspots to treat carefully | |||
| - any status name change | |||
| - any path or folder naming change | |||
| - proof/export file naming conventions | |||
| - any change to custom office copy jobs | |||
| ## 6. Shared Framework Module | |||
| ### What it owns | |||
| - routing and dispatch | |||
| - HTML helpers and form generation | |||
| - anti-CSRF tokens | |||
| - automapping of forms and recordsets | |||
| - utility collections/helpers used across the app | |||
| ### Main files | |||
| - [../MVC/lib.MVC.asp](../MVC/lib.MVC.asp) | |||
| - [../MVC/lib.Routes.asp](../MVC/lib.Routes.asp) | |||
| - [../MVC/lib.HTML.asp](../MVC/lib.HTML.asp) | |||
| - [../MVC/lib.HTMLSecurity.asp](../MVC/lib.HTMLSecurity.asp) | |||
| - [../MVC/lib.Automapper.asp](../MVC/lib.Automapper.asp) | |||
| - [../MVC/lib.all.asp](../MVC/lib.all.asp) | |||
| ### When to open this module | |||
| - form helpers render incorrectly | |||
| - anti-CSRF handling breaks | |||
| - controller dispatch or route generation is wrong | |||
| - multiple features fail in the same infrastructural way | |||
| ## 7. Schema / Data Evolution Module | |||
| ### What it owns | |||
| - database creation and schema changes | |||
| - historical meaning of fields added to kits, jurisdictions, and inkjet records | |||
| ### Main files | |||
| - [../Data/Migrations/migrate.asp](../Data/Migrations/migrate.asp) | |||
| - [../Data/Migrations/lib.Migrations.asp](../Data/Migrations/lib.Migrations.asp) | |||
| - [../Data/Migrations](../Data/Migrations) | |||
| ### Practical rule | |||
| If you add or reinterpret a field in a repository, search migrations first and then search controllers and import scripts for that field name. In this project, schema semantics leak into UI, export logic, and batch automation very quickly. | |||
| ## Recommended Read Paths By Change Type | |||
| ### “Add a new field to kit workflow” | |||
| Open: | |||
| - [../Data/Migrations](../Data/Migrations) | |||
| - [../App/DomainModels/KitRepository.asp](../App/DomainModels/KitRepository.asp) | |||
| - [../App/ViewModels/KitViewModels.asp](../App/ViewModels/KitViewModels.asp) | |||
| - [../App/Controllers/Kit/KitController.asp](../App/Controllers/Kit/KitController.asp) | |||
| - [../App/Views/Kit](../App/Views/Kit) | |||
| ### “Change jurisdiction import behavior” | |||
| Open: | |||
| - [../App/Controllers/Jurisdiction/JurisdictionController.asp](../App/Controllers/Jurisdiction/JurisdictionController.asp) | |||
| - [../App/Views/Jurisdiction/import.asp](../App/Views/Jurisdiction/import.asp) | |||
| - [../App/DomainModels/JurisdictionRepository.asp](../App/DomainModels/JurisdictionRepository.asp) | |||
| ### “Change export file format or proof output” | |||
| Open: | |||
| - [../App/Controllers/Kit/KitController.asp](../App/Controllers/Kit/KitController.asp) | |||
| - [../App/DomainModels/SnailWorksRepository.asp](../App/DomainModels/SnailWorksRepository.asp) | |||
| - [../ImportService/TrackingDataImport.vbs](../ImportService/TrackingDataImport.vbs) | |||
| - [../Data](../Data) | |||
| ### “Change purple-envelope or inkjet assignment logic” | |||
| Open: | |||
| - [../App/Controllers/Kit/KitController.asp](../App/Controllers/Kit/KitController.asp) | |||
| - [../App/DomainModels/InkjetRecordsRepository.asp](../App/DomainModels/InkjetRecordsRepository.asp) | |||
| - [../App/Views/Kit/SwitchBoardPurpleEnvelopeEdit.asp](../App/Views/Kit/SwitchBoardPurpleEnvelopeEdit.asp) | |||
| - [../App/DomainModels/ColorsRepository.asp](../App/DomainModels/ColorsRepository.asp) | |||
| --- | |||
| _Generated as a focused brownfield navigation guide._ | |||
| @@ -0,0 +1,88 @@ | |||
| # tracking_kits - Project Overview | |||
| **Date:** 2026-03-13 | |||
| **Type:** Web application | |||
| **Architecture:** Classic ASP MVC-style monolith | |||
| ## Executive Summary | |||
| This project manages operational workflows for election mail tracking kits. Users browse jurisdictions, create tracking kits, print or export proof artifacts, manage purple envelope and custom office copy jobs, generate inkjet-ready files, and produce export files for online mail tracking. The app is tightly coupled to Windows infrastructure, IIS, COM/ActiveX components, and an Access database. | |||
| ## Project Classification | |||
| - **Repository Type:** Monolith | |||
| - **Project Type:** Classic ASP server-rendered web app | |||
| - **Primary Languages:** VBScript, ASP, SQL, VBScript automation | |||
| - **Architecture Pattern:** Controller + View + Repository over shared MVC helper libraries | |||
| ## Technology Stack Summary | |||
| | Category | Technology | Notes | | |||
| | --- | --- | --- | | |||
| | Web runtime | IIS + Classic ASP | `index.asp` redirects into controller ASP files | | |||
| | Server language | VBScript | `Option Explicit` is used in application files | | |||
| | MVC framework | Custom `/MVC` helpers | Routing, form cache, HTML security, flash, automapper | | |||
| | Data access | ADO via `lib.DAL.asp` | Repository classes issue SQL directly | | |||
| | Database | Access MDB via Jet/ACE | Sample MDB files exist in `Data/`; migrations are ASP/SQL scripts | | |||
| | Reporting | ReportMan ActiveX | Used for proofs and label PDFs | | |||
| | PDF/export | Debenu PDF Library ActiveX | Used by import/export automation | | |||
| | External integration | Chilkat COM components | FTP/SFTP and CSV-related utilities appear in automation/config | | |||
| | Testing | ASPUnit | Test runner is served through IIS | | |||
| | Automation | VBScript and PowerShell | Import service and helper scripts live outside the web request path | | |||
| ## Key Features | |||
| - Jurisdiction browsing and search by `JCode`, `Name`, address, and IMB-related fields | |||
| - Tracking kit creation and maintenance | |||
| - Switchboard views for tracking kits and purple envelope kits | |||
| - Label and proof generation through report templates in `Data/*.rep` | |||
| - Inkjet record management and color assignment | |||
| - SnailWorks export generation | |||
| - Import-service automation for processing tracking data outside the web app | |||
| ## Architecture Highlights | |||
| - Requests enter through controller `.asp` files directly, not a centralized front controller. | |||
| - Shared includes in [../App/include_all.asp](../App/include_all.asp) act as the real application bootstrap. | |||
| - Repositories own SQL and data mapping responsibilities. | |||
| - Views are server-side `.asp` templates rendered from controller methods. | |||
| - The runtime assumes file-system side effects: creating folders, writing PDFs/CSVs, and moving export files. | |||
| - The app has environment-specific behavior embedded in code, especially in [../App/app.config.asp](../App/app.config.asp) and [../ImportService/TrackingDataImport.vbs](../ImportService/TrackingDataImport.vbs). | |||
| ## Development Overview | |||
| ### Prerequisites | |||
| - Windows + IIS + Classic ASP | |||
| - Access/Jet or ACE provider | |||
| - Registered COM dependencies for reporting/export features | |||
| ### Getting Started | |||
| The repo can be edited anywhere, but meaningful runtime verification requires IIS on Windows. The Linux workspace is sufficient for code review and documentation generation, not end-to-end proof/export execution. | |||
| ### Key Commands | |||
| - **Run app:** Serve repo root from IIS | |||
| - **Run tests:** Open `Tests/Test_All.asp` | |||
| - **Run import service manually:** `cscript ImportService/TrackingDataImport.vbs` | |||
| ## Repository Structure | |||
| - `App/` contains the application logic, views, and repositories. | |||
| - `MVC/` contains the shared Classic ASP framework code. | |||
| - `Data/` contains migrations, report templates, and sample assets. | |||
| - `ImportService/` contains out-of-band processing logic. | |||
| - `Tests/` contains helper-library tests via ASPUnit. | |||
| - `Dependancies/` holds external binaries and COM-related artifacts needed in production-like environments. | |||
| ## Documentation Map | |||
| - [index.md](./index.md) - master documentation index | |||
| - [architecture.md](./architecture.md) - detailed technical architecture | |||
| - [source-tree-analysis.md](./source-tree-analysis.md) - directory structure and entry points | |||
| - [development-guide.md](./development-guide.md) - local development workflow | |||
| --- | |||
| _Generated for brownfield analysis._ | |||
| @@ -0,0 +1 @@ | |||
| {"workflow_version":"1.2.0","timestamps":{"started":"2026-03-13T00:00:00Z","last_updated":"2026-03-13T00:00:00Z"},"mode":"initial_scan","scan_level":"deep","project_root":"/workspace","project_knowledge":"/workspace/docs","completed_steps":[{"step":"step_1","status":"completed","timestamp":"2026-03-13T00:00:00Z","summary":"Classified as monolith with 1 part: Classic ASP web application"},{"step":"step_2","status":"completed","timestamp":"2026-03-13T00:00:00Z","summary":"Discovered existing repo README and AGENTS guidance"},{"step":"step_3","status":"completed","timestamp":"2026-03-13T00:00:00Z","summary":"Identified Classic ASP MVC, ADO/Access, IIS, ASPUnit, VBScript automation, and COM dependencies"},{"step":"step_4","status":"completed","timestamp":"2026-03-13T00:00:00Z","summary":"Documented controller routes, data model tables, UI surfaces, config patterns, import/export flows, and Windows-only integrations"},{"step":"step_5","status":"completed","timestamp":"2026-03-13T00:00:00Z","summary":"Generated annotated source tree analysis"},{"step":"step_6","status":"completed","timestamp":"2026-03-13T00:00:00Z","summary":"Generated development and deployment guidance"},{"step":"step_8","status":"completed","timestamp":"2026-03-13T00:00:00Z","summary":"Generated single-part architecture documentation"},{"step":"step_9","status":"completed","timestamp":"2026-03-13T00:00:00Z","summary":"Generated supporting documents for overview, components, APIs, and data models"},{"step":"step_10","status":"completed","timestamp":"2026-03-13T00:00:00Z","summary":"Generated master index for brownfield documentation"}],"current_step":"complete","findings":{"project_classification":"Monolithic Classic ASP web application with MVC-style folder layout and Windows-specific automation.","technology_stack":"VBScript/Classic ASP, IIS, ADO with Access/Jet or ACE, ReportMan, Chilkat, Debenu PDF Library, ASPUnit.","batches_completed":[{"path":"/workspace/App","files_scanned":61,"summary":"Core MVC application: controllers, views, repositories, DAL, and view models."},{"path":"/workspace/Data","files_scanned":33,"summary":"Report templates, sample assets, migrations, and sample MDB data."},{"path":"/workspace/ImportService","files_scanned":2,"summary":"VBScript automation for imports, proofs, and export processing."},{"path":"/workspace/MVC","files_scanned":19,"summary":"Shared Classic ASP MVC helper framework and utility libraries."},{"path":"/workspace/Tests","files_scanned":16,"summary":"ASPUnit runner plus helper/library tests and an import test harness."}],"project_types":[{"part_id":"app","project_type_id":"web","display_name":"Classic ASP web app"}]},"outputs_generated":["project-scan-report.json","index.md","project-overview.md","source-tree-analysis.md","architecture.md","development-guide.md","component-inventory.md","data-models.md","api-contracts.md"],"resume_instructions":"Documentation run completed. Re-run document-project to rescan or add deep-dive docs for a specific module."} | |||
| @@ -0,0 +1,148 @@ | |||
| # tracking_kits - Source Tree Analysis | |||
| **Date:** 2026-03-13 | |||
| ## Overview | |||
| This repository is organized as a single Classic ASP application plus supporting automation and documentation assets. The web app lives under `App/` and depends on shared infrastructure in `MVC/`, database/report assets in `Data/`, and Windows-specific automation under `ImportService/`. | |||
| ## Complete Directory Structure | |||
| ```text | |||
| /workspace | |||
| |-- index.asp | |||
| |-- web.config | |||
| |-- README.md | |||
| |-- App/ | |||
| | |-- app.config.asp | |||
| | |-- include_all.asp | |||
| | |-- Controllers/ | |||
| | |-- DAL/ | |||
| | |-- DomainModels/ | |||
| | |-- ViewModels/ | |||
| | `-- Views/ | |||
| |-- MVC/ | |||
| |-- Data/ | |||
| | |-- Migrations/ | |||
| | `-- *.rep / sample assets / sample MDB files | |||
| |-- ImportService/ | |||
| |-- Tests/ | |||
| | |-- ASPUnit/ | |||
| | `-- Test_*.asp / TestCase_*.asp | |||
| |-- Dependancies/ | |||
| |-- dist/ | |||
| |-- uploads/ | |||
| `-- _bmad-output/ | |||
| ``` | |||
| ## Critical Directories | |||
| ### `App/Controllers` | |||
| **Purpose:** HTTP request handling and orchestration. | |||
| **Contains:** 8 controller files for `Home`, `Kit`, `Jurisdiction`, `InkjetRecords`, `Contacts`, `Settings`, `CustomOfficeCopyJob`, and `KitLabels`. | |||
| **Entry Points:** `App/Controllers/Home/HomeController.asp`, `App/Controllers/Kit/KitController.asp` | |||
| ### `App/DomainModels` | |||
| **Purpose:** Repository layer and model classes. | |||
| **Contains:** SQL-backed repositories for jurisdictions, kits, labels, inkjet records, settings, contacts, SnailWorks exports, custom office copy jobs, and colors. | |||
| ### `App/Views` | |||
| **Purpose:** Server-rendered UI templates. | |||
| **Contains:** View folders that mirror controllers, plus shared header/footer layout includes. | |||
| ### `App/DAL` | |||
| **Purpose:** Database connectivity and low-level query execution. | |||
| **Contains:** `lib.DAL.asp`, the ADO abstraction used by repositories. | |||
| ### `MVC` | |||
| **Purpose:** Shared framework and utility code. | |||
| **Contains:** Routing, HTML helpers, CSRF handling, flash messages, automapper, collections, strings, validation, upload, and JSON support. | |||
| ### `Data/Migrations` | |||
| **Purpose:** Database schema evolution. | |||
| **Contains:** 20 numbered migration scripts, migration helpers, and bootstrap SQL/scripts. | |||
| ### `Data` | |||
| **Purpose:** Reporting templates and sample data artifacts. | |||
| **Contains:** `.rep` templates, images, PDFs, PowerShell utilities, and sample MDB files. | |||
| ### `ImportService` | |||
| **Purpose:** Out-of-band workflow processing. | |||
| **Contains:** `TrackingDataImport.vbs` and related service installers for importing, proving, and exporting jobs outside web requests. | |||
| ### `Tests` | |||
| **Purpose:** ASPUnit-based verification. | |||
| **Contains:** Test runner, helper tests, and a VBScript harness for import-related behavior. | |||
| ## Entry Points | |||
| - **Main web entry:** [../index.asp](../index.asp) | |||
| - **Route bootstrap:** [../App/app.config.asp](../App/app.config.asp) | |||
| - **Shared include bootstrap:** [../App/include_all.asp](../App/include_all.asp) | |||
| - **Import automation entry:** [../ImportService/TrackingDataImport.vbs](../ImportService/TrackingDataImport.vbs) | |||
| - **Test runner entry:** [../Tests/Test_All.asp](../Tests/Test_All.asp) | |||
| ## File Organization Patterns | |||
| - Controllers, view models, views, and repositories are grouped by domain area. | |||
| - Most business logic is controller-driven, with repositories focused on persistence and record mapping. | |||
| - Shared runtime behavior is hidden in framework includes rather than dependency injection or explicit composition. | |||
| - The app uses direct file includes extensively; load order matters. | |||
| - Environment configuration is code-based rather than environment-variable based. | |||
| ## Key File Types | |||
| ### `.asp` | |||
| - **Pattern:** `App/**/*.asp`, `MVC/*.asp`, `Tests/*.asp` | |||
| - **Purpose:** Web controllers, views, repositories, framework helpers, and test pages. | |||
| ### `.vbs` | |||
| - **Pattern:** `ImportService/*.vbs`, `App/ScaffoldRepo.vbs` | |||
| - **Purpose:** Batch automation and helper tooling. | |||
| ### `.rep` | |||
| - **Pattern:** `Data/*.rep` | |||
| - **Purpose:** Report definitions used for proofs and PDF generation. | |||
| ### `Migration_*.asp` | |||
| - **Pattern:** `Data/Migrations/Migration_*.asp` | |||
| - **Purpose:** Incremental schema changes implemented in Classic ASP/VBScript. | |||
| ## Configuration Files | |||
| - [../web.config](../web.config) - IIS default document configuration. | |||
| - [../App/app.config.asp](../App/app.config.asp) - Route init, UI asset URLs, `dev` mode, export directory, and COM unlock setup. | |||
| - [../_bmad/bmm/config.yaml](../_bmad/bmm/config.yaml) - BMAD workflow configuration for documentation generation. | |||
| ## Notes for Development | |||
| - `dist/` and `uploads/` are runtime-facing folders but not the core of the app logic. | |||
| - `Dependancies/` is critical for operational parity even though the application code can be read without it. | |||
| - Several workflows depend on network shares and Windows paths that are not testable in this workspace. | |||
| --- | |||
| _Generated for brownfield analysis._ | |||
Powered by TurnKey Linux.