diff --git a/docs/superpowers/plans/2026-04-13-remove-api-excel-export.md b/docs/superpowers/plans/2026-04-13-remove-api-excel-export.md
new file mode 100644
index 0000000..a6c6046
--- /dev/null
+++ b/docs/superpowers/plans/2026-04-13-remove-api-excel-export.md
@@ -0,0 +1,1150 @@
+# Remove API, Export to Excel — Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Remove FabWorks API and database dependencies from ExportDXF; export DXF files + a BOM Excel workbook to a local `Templates` folder, with content-hash-based revision tracking.
+
+**Architecture:** Merge the `feature/fabworks-api` branch for its structural improvements (service layer, async flow, content hashing, EtchBendLines fixes), then replace the API/DB output layer with local file export + ClosedXML Excel writing. A format template system with pluggable `IDrawingInfoExtractor` handles filename generation.
+
+**Tech Stack:** .NET 8.0, WinForms, ClosedXML 0.104.2, SolidWorks COM Interop, EtchBendLines submodule
+
+---
+
+## File Structure
+
+### New Files
+- `ExportDXF/Services/FilenameTemplateParser.cs` — parses `{item_no:N}`, `{part_name}`, `{config}`, `{material}` placeholders
+- `ExportDXF/Services/IDrawingInfoExtractor.cs` — interface for extracting drawing info from document names
+- `ExportDXF/Services/DefaultDrawingInfoExtractor.cs` — fallback: document name + configured suffix
+- `ExportDXF/Services/EquipmentDrawingInfoExtractor.cs` — parses `{EquipmentNo} {DrawingNo}` pattern
+- `ExportDXF/Models/DrawingInfo.cs` — result of drawing info extraction
+- `ExportDXF/Services/ExcelExportService.cs` — reads/writes BOM.xlsx with ClosedXML
+- `ExportDXF/Services/LogFileService.cs` — per-export and app-level log file writing
+- `ExportDXF/Services/IRawBomTableReader.cs` — reads raw BOM table columns/rows from SolidWorks drawings
+- `ExportDXF/Services/RawBomTableReader.cs` — implementation
+
+### Modified Files
+- `ExportDXF/ExportDXF.csproj` — remove EF Core packages, add ClosedXML
+- `ExportDXF/Program.cs` — rewire DI (remove API client, DB context; add new services)
+- `ExportDXF/Forms/MainForm.cs` — replace dropdowns with format template textbox
+- `ExportDXF/Forms/MainForm.Designer.cs` — UI layout changes
+- `ExportDXF/App.config` — remove connection string, add DefaultSuffix
+- `ExportDXF/Models/CutTemplate.cs` — add Revision property
+- `ExportDXF/Models/ExportContext.cs` — replace Equipment/DrawingNo/Title with template + output folder
+- `ExportDXF/Services/DxfExportService.cs` — replace API calls with local file writes + Excel updates
+
+### Deleted Files
+- `ExportDXF/Services/FabWorksApiClient.cs`
+- `ExportDXF/Services/IFabWorksApiClient.cs`
+- `ExportDXF/Services/FabWorksApiDtos.cs`
+- `ExportDXF/Data/ExportDxfDbContext.cs` (if present after merge)
+- `ExportDXF/Data/Migrations/*` (if present after merge)
+- `FabWorks.Core/` (entire project directory, if present after merge)
+- `FabWorks.Api/` (entire project directory, if present after merge)
+
+---
+
+### Task 1: Merge fabworks-api branch and clean up foreign projects
+
+**Files:**
+- Modify: `ExportDXF.sln`
+- Delete: `FabWorks.Core/` and `FabWorks.Api/` directories (if present after merge)
+
+- [ ] **Step 1: Merge the branch**
+
+```bash
+git merge origin/feature/fabworks-api -m "merge: bring fabworks-api structural improvements into master"
+```
+
+Resolve any conflicts favoring the fabworks-api version for files in `ExportDXF/Services/` and `ExportDXF/Models/`. For `ExportDXF.csproj` and `Program.cs`, take fabworks-api version (we'll modify them in later tasks).
+
+- [ ] **Step 2: Remove FabWorks projects from solution and disk**
+
+Check what projects/directories exist after merge:
+
+```bash
+ls -d FabWorks*/ 2>/dev/null
+grep -i "fabworks" ExportDXF.sln
+```
+
+Remove any FabWorks project directories and their solution references:
+
+```bash
+dotnet sln ExportDXF.sln remove FabWorks.Core/FabWorks.Core.csproj 2>/dev/null
+dotnet sln ExportDXF.sln remove FabWorks.Api/FabWorks.Api.csproj 2>/dev/null
+rm -rf FabWorks.Core/ FabWorks.Api/
+```
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add -A
+git commit -m "chore: remove FabWorks.Core and FabWorks.Api projects after merge"
+```
+
+---
+
+### Task 2: Remove API client, database, and EF Core
+
+**Files:**
+- Delete: `ExportDXF/Services/FabWorksApiClient.cs`, `ExportDXF/Services/IFabWorksApiClient.cs`, `ExportDXF/Services/FabWorksApiDtos.cs`
+- Delete: `ExportDXF/Data/` directory (DbContext + migrations)
+- Modify: `ExportDXF/ExportDXF.csproj` — remove EF Core packages, add ClosedXML
+- Modify: `ExportDXF/App.config` — remove connection string and FabWorksApiUrl
+
+- [ ] **Step 1: Delete API client files**
+
+```bash
+rm -f ExportDXF/Services/FabWorksApiClient.cs
+rm -f ExportDXF/Services/IFabWorksApiClient.cs
+rm -f ExportDXF/Services/FabWorksApiDtos.cs
+```
+
+- [ ] **Step 2: Delete database files**
+
+```bash
+rm -rf ExportDXF/Data/
+```
+
+- [ ] **Step 3: Update ExportDXF.csproj**
+
+Remove EF Core packages and add ClosedXML:
+
+```bash
+cd ExportDXF
+dotnet remove package Microsoft.EntityFrameworkCore.SqlServer 2>/dev/null
+dotnet remove package Microsoft.EntityFrameworkCore.Tools 2>/dev/null
+dotnet remove package Microsoft.EntityFrameworkCore.Design 2>/dev/null
+dotnet add package ClosedXML --version 0.104.2
+cd ..
+```
+
+- [ ] **Step 4: Update App.config**
+
+Remove `FabWorksApiUrl` setting and the connection string. Add `DefaultSuffix` setting.
+
+The `appSettings` section should contain:
+
+```xml
+
+
+
+
+```
+
+Remove any `` section entirely.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add -A
+git commit -m "refactor: remove API client, database, and EF Core dependencies"
+```
+
+---
+
+### Task 3: Create DrawingInfo model and IDrawingInfoExtractor
+
+**Files:**
+- Create: `ExportDXF/Models/DrawingInfo.cs`
+- Create: `ExportDXF/Services/IDrawingInfoExtractor.cs`
+- Create: `ExportDXF/Services/DefaultDrawingInfoExtractor.cs`
+- Create: `ExportDXF/Services/EquipmentDrawingInfoExtractor.cs`
+
+- [ ] **Step 1: Create DrawingInfo model**
+
+Create `ExportDXF/Models/DrawingInfo.cs`:
+
+```csharp
+namespace ExportDXF.Models
+{
+ public class DrawingInfo
+ {
+ public string EquipmentNumber { get; set; }
+ public string DrawingNumber { get; set; }
+ public string DefaultTemplate { get; set; }
+ }
+}
+```
+
+- [ ] **Step 2: Create IDrawingInfoExtractor interface**
+
+Create `ExportDXF/Services/IDrawingInfoExtractor.cs`:
+
+```csharp
+using ExportDXF.Models;
+
+namespace ExportDXF.Services
+{
+ public interface IDrawingInfoExtractor
+ {
+ bool TryExtract(string documentName, out DrawingInfo info);
+ }
+}
+```
+
+- [ ] **Step 3: Create EquipmentDrawingInfoExtractor**
+
+Create `ExportDXF/Services/EquipmentDrawingInfoExtractor.cs`:
+
+```csharp
+using System.Text.RegularExpressions;
+using ExportDXF.Models;
+
+namespace ExportDXF.Services
+{
+ public class EquipmentDrawingInfoExtractor : IDrawingInfoExtractor
+ {
+ private static readonly Regex Pattern = new Regex(
+ @"^(?\d+)\s+(?[A-Za-z]\d+)",
+ RegexOptions.Compiled);
+
+ public bool TryExtract(string documentName, out DrawingInfo info)
+ {
+ info = null;
+
+ // Strip file extension
+ var name = Path.GetFileNameWithoutExtension(documentName);
+
+ var match = Pattern.Match(name);
+ if (!match.Success)
+ return false;
+
+ var equip = match.Groups["equip"].Value;
+ var drawing = match.Groups["drawing"].Value;
+
+ info = new DrawingInfo
+ {
+ EquipmentNumber = equip,
+ DrawingNumber = drawing,
+ DefaultTemplate = $"{equip} {drawing} PT{{item_no:2}}"
+ };
+
+ return true;
+ }
+ }
+}
+```
+
+- [ ] **Step 4: Create DefaultDrawingInfoExtractor**
+
+Create `ExportDXF/Services/DefaultDrawingInfoExtractor.cs`:
+
+```csharp
+using System.Configuration;
+using ExportDXF.Models;
+
+namespace ExportDXF.Services
+{
+ public class DefaultDrawingInfoExtractor : IDrawingInfoExtractor
+ {
+ public bool TryExtract(string documentName, out DrawingInfo info)
+ {
+ var name = Path.GetFileNameWithoutExtension(documentName);
+ var suffix = ConfigurationManager.AppSettings["DefaultSuffix"] ?? "PT{item_no:2}";
+
+ info = new DrawingInfo
+ {
+ EquipmentNumber = null,
+ DrawingNumber = null,
+ DefaultTemplate = $"{name} {suffix}"
+ };
+
+ return true;
+ }
+ }
+}
+```
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add ExportDXF/Models/DrawingInfo.cs ExportDXF/Services/IDrawingInfoExtractor.cs ExportDXF/Services/DefaultDrawingInfoExtractor.cs ExportDXF/Services/EquipmentDrawingInfoExtractor.cs
+git commit -m "feat: add IDrawingInfoExtractor with equipment and default implementations"
+```
+
+---
+
+### Task 4: Create FilenameTemplateParser
+
+**Files:**
+- Create: `ExportDXF/Services/FilenameTemplateParser.cs`
+
+- [ ] **Step 1: Create FilenameTemplateParser**
+
+Create `ExportDXF/Services/FilenameTemplateParser.cs`:
+
+```csharp
+using System.Text.RegularExpressions;
+using ExportDXF.Models;
+
+namespace ExportDXF.Services
+{
+ public static class FilenameTemplateParser
+ {
+ private static readonly Regex PlaceholderPattern = new Regex(
+ @"\{(?\w+)(?::(?\d+))?\}",
+ RegexOptions.Compiled);
+
+ ///
+ /// Evaluates a template string for a given item.
+ /// e.g. "4321 A01 PT{item_no:2}" with item_no=3 → "4321 A01 PT03"
+ ///
+ public static string Evaluate(string template, Item item)
+ {
+ return PlaceholderPattern.Replace(template, match =>
+ {
+ var name = match.Groups["name"].Value.ToLowerInvariant();
+ var padStr = match.Groups["pad"].Value;
+ int pad = string.IsNullOrEmpty(padStr) ? 0 : int.Parse(padStr);
+
+ string value;
+ switch (name)
+ {
+ case "item_no":
+ value = item.ItemNo.ToString();
+ if (pad > 0)
+ value = value.PadLeft(pad, '0');
+ break;
+ case "part_name":
+ value = item.PartName ?? "";
+ break;
+ case "config":
+ value = item.Configuration ?? "";
+ break;
+ case "material":
+ value = item.Material ?? "";
+ break;
+ default:
+ value = match.Value; // leave unknown placeholders as-is
+ break;
+ }
+
+ return value;
+ });
+ }
+
+ ///
+ /// Extracts the literal prefix before the first placeholder.
+ /// Used for naming the xlsx and log files.
+ /// Falls back to documentName if prefix is empty.
+ ///
+ public static string GetPrefix(string template, string documentName)
+ {
+ var match = PlaceholderPattern.Match(template);
+ if (!match.Success)
+ return template.Trim();
+
+ var prefix = template.Substring(0, match.Index).Trim();
+ if (string.IsNullOrEmpty(prefix))
+ return Path.GetFileNameWithoutExtension(documentName);
+
+ return prefix;
+ }
+
+ ///
+ /// Validates that the template contains {item_no} to prevent filename collisions.
+ ///
+ public static bool Validate(string template, out string error)
+ {
+ error = null;
+
+ if (string.IsNullOrWhiteSpace(template))
+ {
+ error = "Template cannot be empty.";
+ return false;
+ }
+
+ var hasItemNo = PlaceholderPattern.IsMatch(template) &&
+ PlaceholderPattern.Matches(template)
+ .Cast()
+ .Any(m => m.Groups["name"].Value.ToLowerInvariant() == "item_no");
+
+ if (!hasItemNo)
+ {
+ error = "Template must contain {item_no} or {item_no:N} to avoid filename collisions.";
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add ExportDXF/Services/FilenameTemplateParser.cs
+git commit -m "feat: add FilenameTemplateParser with placeholder evaluation and validation"
+```
+
+---
+
+### Task 5: Update CutTemplate model with Revision
+
+**Files:**
+- Modify: `ExportDXF/Models/CutTemplate.cs`
+
+- [ ] **Step 1: Add Revision property to CutTemplate**
+
+Add `Revision` property (int, default 1) to `CutTemplate.cs`. Remove any EF Core attributes or database-specific annotations (foreign keys to ExportRecord, etc.):
+
+```csharp
+namespace ExportDXF.Models
+{
+ public class CutTemplate
+ {
+ public string DxfFilePath { get; set; }
+ public string ContentHash { get; set; }
+ public string CutTemplateName { get; set; }
+ public int Revision { get; set; } = 1;
+ public double Thickness { get; set; }
+ public double KFactor { get; set; }
+ public double DefaultBendRadius { get; set; }
+ public int BomItemId { get; set; }
+ }
+}
+```
+
+- [ ] **Step 2: Clean up BomItem.cs**
+
+Remove any EF Core navigation properties or database annotations. Keep the data properties:
+
+```csharp
+namespace ExportDXF.Models
+{
+ public class BomItem
+ {
+ public int ItemNo { get; set; }
+ public string PartNo { get; set; }
+ public int SortOrder { get; set; }
+ public int Qty { get; set; }
+ public int TotalQty { get; set; }
+ public string Description { get; set; }
+ public string PartName { get; set; }
+ public string ConfigurationName { get; set; }
+ public string Material { get; set; }
+ public CutTemplate CutTemplate { get; set; }
+ }
+}
+```
+
+- [ ] **Step 3: Remove ExportRecord.cs**
+
+The export record model was for DB persistence. Delete it — the log file replaces this purpose:
+
+```bash
+rm -f ExportDXF/Models/ExportRecord.cs
+```
+
+- [ ] **Step 4: Update ExportContext.cs**
+
+Replace API-oriented fields with template and output folder:
+
+```csharp
+using SldWorks;
+
+namespace ExportDXF.Models
+{
+ public class ExportContext
+ {
+ public SolidWorksDocument ActiveDocument { get; set; }
+ public IViewFlipDecider ViewFlipDecider { get; set; }
+ public string FilenameTemplate { get; set; }
+ public string OutputFolder { get; set; }
+ public CancellationToken CancellationToken { get; set; }
+ public Action ProgressCallback { get; set; }
+ public Action BomItemCallback { get; set; }
+ public SldWorks.SldWorks SolidWorksApp { get; set; }
+
+ private DrawingDoc _templateDrawing;
+
+ public DrawingDoc GetOrCreateTemplateDrawing()
+ {
+ if (_templateDrawing != null)
+ return _templateDrawing;
+
+ var templatePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Templates", "Blank.drwdot");
+ var swApp = SolidWorksApp;
+ int errors = 0, warnings = 0;
+ var doc = swApp.NewDocument(templatePath, 0, 0, 0);
+ _templateDrawing = (DrawingDoc)doc;
+ return _templateDrawing;
+ }
+
+ public void CleanupTemplateDrawing()
+ {
+ if (_templateDrawing == null) return;
+ var swApp = SolidWorksApp;
+ swApp.CloseDoc(((ModelDoc2)_templateDrawing).GetTitle());
+ _templateDrawing = null;
+ }
+
+ public void LogProgress(string message, LogLevel level = LogLevel.Info, string file = null)
+ {
+ ProgressCallback?.Invoke(message, level, file);
+ }
+ }
+}
+```
+
+Note: Preserve the exact `GetOrCreateTemplateDrawing` and `CleanupTemplateDrawing` implementations from the fabworks-api branch — the above is approximate. The key change is replacing `Equipment`, `DrawingNo`, `Title`, `FilePrefix`, `EquipmentId` with `FilenameTemplate` and `OutputFolder`.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add -A
+git commit -m "refactor: update models — add CutTemplate.Revision, remove ExportRecord, simplify ExportContext"
+```
+
+---
+
+### Task 6: Create ExcelExportService
+
+**Files:**
+- Create: `ExportDXF/Services/ExcelExportService.cs`
+
+- [ ] **Step 1: Create ExcelExportService**
+
+Create `ExportDXF/Services/ExcelExportService.cs`:
+
+```csharp
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using ClosedXML.Excel;
+using ExportDXF.Models;
+
+namespace ExportDXF.Services
+{
+ public class ExcelExportService
+ {
+ ///
+ /// Reads existing Cut Templates from an xlsx file to compare content hashes.
+ /// Returns empty dictionary if file doesn't exist.
+ /// Key = Item #, Value = (ContentHash, Revision, FileName)
+ ///
+ public Dictionary ReadExistingCutTemplates(string xlsxPath)
+ {
+ var result = new Dictionary();
+
+ if (!File.Exists(xlsxPath))
+ return result;
+
+ using (var workbook = new XLWorkbook(xlsxPath))
+ {
+ if (!workbook.TryGetWorksheet("Cut Templates", out var ws))
+ return result;
+
+ var headerRow = ws.Row(1);
+ var headers = new Dictionary();
+ for (int col = 1; col <= ws.LastColumnUsed()?.ColumnNumber() ?? 0; col++)
+ {
+ headers[headerRow.Cell(col).GetString()] = col;
+ }
+
+ if (!headers.ContainsKey("Item #") || !headers.ContainsKey("Content Hash"))
+ return result;
+
+ for (int row = 2; row <= ws.LastRowUsed()?.RowNumber() ?? 1; row++)
+ {
+ var itemNo = ws.Cell(row, headers["Item #"]).GetValue();
+ var hash = ws.Cell(row, headers["Content Hash"]).GetString();
+ var revision = headers.ContainsKey("Revision")
+ ? ws.Cell(row, headers["Revision"]).GetValue()
+ : 1;
+ var fileName = headers.ContainsKey("File Name")
+ ? ws.Cell(row, headers["File Name"]).GetString()
+ : "";
+
+ result[itemNo] = (hash, revision, fileName);
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// Writes or updates the xlsx file with BOM and Cut Templates sheets.
+ /// rawBomTable: list of rows where each row is a dictionary of column name → value.
+ /// If null, the BOM sheet is not written (Part/Assembly exports).
+ ///
+ public void Write(
+ string xlsxPath,
+ List> rawBomTable,
+ List bomItems)
+ {
+ using (var workbook = File.Exists(xlsxPath)
+ ? new XLWorkbook(xlsxPath)
+ : new XLWorkbook())
+ {
+ // BOM sheet — full rewrite from drawing BOM table
+ if (rawBomTable != null && rawBomTable.Count > 0)
+ {
+ if (workbook.TryGetWorksheet("BOM", out var existingBom))
+ workbook.Worksheets.Delete("BOM");
+
+ var bomSheet = workbook.Worksheets.Add("BOM");
+ var columns = rawBomTable[0].Keys.ToList();
+
+ // Headers
+ for (int col = 0; col < columns.Count; col++)
+ bomSheet.Cell(1, col + 1).Value = columns[col];
+
+ // Data rows
+ for (int row = 0; row < rawBomTable.Count; row++)
+ {
+ for (int col = 0; col < columns.Count; col++)
+ {
+ var value = rawBomTable[row].GetValueOrDefault(columns[col], "");
+ bomSheet.Cell(row + 2, col + 1).Value = value;
+ }
+ }
+
+ bomSheet.Columns().AdjustToContents();
+ }
+
+ // Cut Templates sheet — full rewrite with current data
+ if (workbook.TryGetWorksheet("Cut Templates", out var existingCt))
+ workbook.Worksheets.Delete("Cut Templates");
+
+ var ctSheet = workbook.Worksheets.Add("Cut Templates");
+
+ // Headers
+ var ctHeaders = new[] { "Item #", "File Name", "Revision", "Thickness", "K-Factor", "Bend Radius", "Content Hash" };
+ for (int col = 0; col < ctHeaders.Length; col++)
+ ctSheet.Cell(1, col + 1).Value = ctHeaders[col];
+
+ // Data rows — only BOM items that have a CutTemplate
+ int ctRow = 2;
+ foreach (var item in bomItems.Where(b => b.CutTemplate != null).OrderBy(b => b.ItemNo))
+ {
+ var ct = item.CutTemplate;
+ ctSheet.Cell(ctRow, 1).Value = item.ItemNo;
+ ctSheet.Cell(ctRow, 2).Value = ct.DxfFilePath;
+ ctSheet.Cell(ctRow, 3).Value = ct.Revision;
+ ctSheet.Cell(ctRow, 4).Value = ct.Thickness;
+ ctSheet.Cell(ctRow, 5).Value = ct.KFactor;
+ ctSheet.Cell(ctRow, 6).Value = ct.DefaultBendRadius;
+ ctSheet.Cell(ctRow, 7).Value = ct.ContentHash;
+ ctRow++;
+ }
+
+ ctSheet.Columns().AdjustToContents();
+
+ workbook.SaveAs(xlsxPath);
+ }
+ }
+ }
+}
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add ExportDXF/Services/ExcelExportService.cs
+git commit -m "feat: add ExcelExportService for BOM and Cut Templates xlsx output"
+```
+
+---
+
+### Task 7: Create LogFileService
+
+**Files:**
+- Create: `ExportDXF/Services/LogFileService.cs`
+
+- [ ] **Step 1: Create LogFileService**
+
+Create `ExportDXF/Services/LogFileService.cs`:
+
+```csharp
+using System;
+using System.IO;
+
+namespace ExportDXF.Services
+{
+ public class LogFileService : IDisposable
+ {
+ private StreamWriter _exportLog;
+ private static readonly string AppLogPath = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+ "ExportDXF", "ExportDXF.log");
+
+ public void StartExportLog(string logFilePath)
+ {
+ var dir = Path.GetDirectoryName(logFilePath);
+ if (!Directory.Exists(dir))
+ Directory.CreateDirectory(dir);
+
+ _exportLog = new StreamWriter(logFilePath, append: true);
+ }
+
+ public void Log(string level, string message)
+ {
+ var line = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [{level}] {message}";
+
+ _exportLog?.WriteLine(line);
+ _exportLog?.Flush();
+
+ WriteAppLog(line);
+ }
+
+ public void LogInfo(string message) => Log("INFO", message);
+ public void LogWarning(string message) => Log("WARNING", message);
+ public void LogError(string message) => Log("ERROR", message);
+
+ private void WriteAppLog(string line)
+ {
+ var dir = Path.GetDirectoryName(AppLogPath);
+ if (!Directory.Exists(dir))
+ Directory.CreateDirectory(dir);
+
+ File.AppendAllText(AppLogPath, line + Environment.NewLine);
+ }
+
+ public void Dispose()
+ {
+ _exportLog?.Dispose();
+ _exportLog = null;
+ }
+ }
+}
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add ExportDXF/Services/LogFileService.cs
+git commit -m "feat: add LogFileService for per-export and app-level logging"
+```
+
+---
+
+### Task 8: Create RawBomTableReader
+
+**Files:**
+- Create: `ExportDXF/Services/RawBomTableReader.cs`
+
+- [ ] **Step 1: Create RawBomTableReader**
+
+This reads the SolidWorks BOM table as raw column/row data (all visible columns, all visible rows) for direct copy into the Excel BOM sheet.
+
+Create `ExportDXF/Services/RawBomTableReader.cs`:
+
+```csharp
+using System.Collections.Generic;
+using SolidWorks.Interop.sldworks;
+using SolidWorks.Interop.swconst;
+
+namespace ExportDXF.Services
+{
+ public static class RawBomTableReader
+ {
+ ///
+ /// Reads all visible columns and rows from a BOM table annotation.
+ /// Returns a list of rows, each row a dictionary of column header → cell value.
+ ///
+ public static List> Read(ITableAnnotation table)
+ {
+ var rows = new List>();
+
+ int colCount = table.ColumnCount;
+ int rowCount = table.RowCount;
+
+ // Build column headers (visible only)
+ var columns = new List<(int Index, string Header)>();
+ for (int col = 0; col < colCount; col++)
+ {
+ if (table.ColumnHidden[col])
+ continue;
+
+ var header = table.get_Text(0, col)?.Trim() ?? $"Column{col}";
+ columns.Add((col, header));
+ }
+
+ // Read data rows (skip header row 0, skip hidden rows)
+ for (int row = 1; row < rowCount; row++)
+ {
+ if (table.RowHidden[row])
+ continue;
+
+ var rowData = new Dictionary();
+ foreach (var (colIdx, header) in columns)
+ {
+ rowData[header] = table.get_Text(row, colIdx)?.Trim() ?? "";
+ }
+ rows.Add(rowData);
+ }
+
+ return rows;
+ }
+ }
+}
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add ExportDXF/Services/RawBomTableReader.cs
+git commit -m "feat: add RawBomTableReader for copying SolidWorks BOM tables to Excel"
+```
+
+---
+
+### Task 9: Update DxfExportService for local export
+
+**Files:**
+- Modify: `ExportDXF/Services/DxfExportService.cs`
+
+This is the largest change. The service currently calls the API for file uploads, record creation, and BOM item persistence. We replace all of that with:
+- Write DXF files directly to the output folder
+- Use FilenameTemplateParser for naming
+- Use ExcelExportService for revision tracking and xlsx output
+- Use LogFileService for logging
+
+- [ ] **Step 1: Update DxfExportService constructor and dependencies**
+
+Replace `IFabWorksApiClient` dependency with `ExcelExportService` and `LogFileService`. Remove all API-related fields.
+
+The constructor should accept:
+
+```csharp
+public DxfExportService(
+ IPartExporter partExporter,
+ IDrawingExporter drawingExporter,
+ IBomExtractor bomExtractor,
+ ExcelExportService excelExportService,
+ LogFileService logFileService)
+```
+
+- [ ] **Step 2: Rewrite ExportAsync to use local output**
+
+The main export flow becomes:
+
+```csharp
+public async Task ExportAsync(ExportContext context)
+{
+ var outputFolder = context.OutputFolder;
+ if (!Directory.Exists(outputFolder))
+ Directory.CreateDirectory(outputFolder);
+
+ var prefix = FilenameTemplateParser.GetPrefix(
+ context.FilenameTemplate,
+ context.ActiveDocument.Title);
+
+ var xlsxPath = Path.Combine(outputFolder, $"{prefix}.xlsx");
+ var logPath = Path.Combine(outputFolder, $"{prefix}.log");
+
+ _logFileService.StartExportLog(logPath);
+ _logFileService.LogInfo($"Export started: {context.ActiveDocument.FilePath}");
+
+ // Read existing cut templates for revision comparison
+ var existingTemplates = _excelExportService.ReadExistingCutTemplates(xlsxPath);
+
+ var bomItems = new List();
+ List> rawBomTable = null;
+
+ try
+ {
+ switch (context.ActiveDocument.DocumentType)
+ {
+ case DocumentType.Part:
+ await ExportPartAsync(context, outputFolder, existingTemplates, bomItems);
+ break;
+ case DocumentType.Assembly:
+ await ExportAssemblyAsync(context, outputFolder, existingTemplates, bomItems);
+ break;
+ case DocumentType.Drawing:
+ rawBomTable = await ExportDrawingAsync(context, outputFolder, existingTemplates, bomItems);
+ break;
+ }
+
+ // Write Excel file
+ _excelExportService.Write(xlsxPath, rawBomTable, bomItems);
+ _logFileService.LogInfo($"Wrote {xlsxPath}");
+ }
+ catch (OperationCanceledException)
+ {
+ _logFileService.LogWarning("Export cancelled by user");
+ throw;
+ }
+ catch (Exception ex)
+ {
+ _logFileService.LogError($"Export failed: {ex.Message}");
+ throw;
+ }
+ finally
+ {
+ context.CleanupTemplateDrawing();
+ }
+}
+```
+
+- [ ] **Step 3: Rewrite ExportItemsAsync for local file output with revision tracking**
+
+For each item that is sheet metal:
+
+1. Export DXF to temp location via `_partExporter`
+2. Run EtchBendLines processing
+3. Compute content hash
+4. Check `existingTemplates` for matching item number:
+ - **Hash matches** → skip file write, reuse existing filename and revision
+ - **Hash differs** → increment revision, write file with revision suffix (e.g., `PT03 Rev2.dxf`)
+ - **New item** → write file, revision = 1
+5. Create BomItem + CutTemplate, invoke callback
+
+The DXF filename is generated by `FilenameTemplateParser.Evaluate(context.FilenameTemplate, item)`.
+
+Revision suffix logic:
+
+```csharp
+private string GetDxfFileName(string baseName, int revision)
+{
+ if (revision <= 1)
+ return $"{baseName}.dxf";
+ return $"{baseName} Rev{revision}.dxf";
+}
+```
+
+- [ ] **Step 4: Update ExportDrawingAsync to read raw BOM table**
+
+Before extracting items, read the raw BOM table for the Excel BOM sheet:
+
+```csharp
+private async Task>> ExportDrawingAsync(
+ ExportContext context,
+ string outputFolder,
+ Dictionary existingTemplates,
+ List bomItems)
+{
+ var drawingDoc = (DrawingDoc)context.ActiveDocument.NativeDocument;
+
+ // Read raw BOM table for Excel output
+ var rawBomTable = new List>();
+ var bomFeature = (BomFeature)/* get BOM feature from drawing */;
+ // Use existing BomExtractor to get the table annotations, then RawBomTableReader.Read()
+
+ // Export PDF
+ await Task.Run(() => _drawingExporter.ExportToPdf(drawingDoc, outputFolder, context));
+
+ // Extract and export items
+ var items = _bomExtractor.ExtractFromDrawing(drawingDoc, context.ProgressCallback);
+ await ExportItemsAsync(items, context, outputFolder, existingTemplates, bomItems);
+
+ return rawBomTable;
+}
+```
+
+Note: The exact SolidWorks API calls to get BOM table annotations should be preserved from the existing BomExtractor code. Add a call to `RawBomTableReader.Read()` alongside the existing item extraction.
+
+- [ ] **Step 5: Remove all API client calls**
+
+Search for any remaining references to `_apiClient`, `FabWorksApiClient`, `IFabWorksApiClient`, `ApiExportDetail`, etc. and remove them. The service should have zero references to the API.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add -A
+git commit -m "refactor: rewrite DxfExportService for local file export with revision tracking"
+```
+
+---
+
+### Task 10: Update MainForm UI
+
+**Files:**
+- Modify: `ExportDXF/Forms/MainForm.cs`
+- Modify: `ExportDXF/Forms/MainForm.Designer.cs`
+
+- [ ] **Step 1: Update MainForm.Designer.cs**
+
+Replace the equipment and drawing combo boxes with a single format template text box:
+
+- Remove: `cboEquipment`, `cboDrawing`, `lblEquipment`, `lblDrawing`, `txtTitle`
+- Add: `txtFilenameTemplate` (TextBox), `lblFilenameTemplate` (Label with text "Filename Template:")
+- Position the template textbox where the dropdowns were, spanning the full width
+
+- [ ] **Step 2: Update MainForm.cs constructor and fields**
+
+Replace `IFabWorksApiClient` with `IDrawingInfoExtractor[]` (list of extractors):
+
+```csharp
+private readonly IDxfExportService _exportService;
+private readonly IDrawingInfoExtractor[] _extractors;
+
+public MainForm(
+ SolidWorksService solidWorksService,
+ IDxfExportService exportService,
+ IDrawingInfoExtractor[] extractors)
+{
+ InitializeComponent();
+ _solidWorksService = solidWorksService;
+ _exportService = exportService;
+ _extractors = extractors;
+ // ... rest of init
+}
+```
+
+- [ ] **Step 3: Update document change handler for auto-fill**
+
+When the active SolidWorks document changes, try each extractor:
+
+```csharp
+private void OnActiveDocumentChanged(SolidWorksDocument doc)
+{
+ if (doc == null)
+ {
+ txtFilenameTemplate.Text = "";
+ return;
+ }
+
+ foreach (var extractor in _extractors)
+ {
+ if (extractor.TryExtract(doc.Title, out var info))
+ {
+ txtFilenameTemplate.Text = info.DefaultTemplate;
+ return;
+ }
+ }
+}
+```
+
+- [ ] **Step 4: Update StartExportAsync**
+
+Build the ExportContext with template and output folder:
+
+```csharp
+private async Task StartExportAsync()
+{
+ var template = txtFilenameTemplate.Text.Trim();
+ if (!FilenameTemplateParser.Validate(template, out var error))
+ {
+ MessageBox.Show(error, "Invalid Template", MessageBoxButtons.OK, MessageBoxIcon.Warning);
+ return;
+ }
+
+ var doc = _solidWorksService.GetActiveDocument();
+ var sourceDir = Path.GetDirectoryName(doc.FilePath);
+ var outputFolder = Path.Combine(sourceDir, "Templates");
+
+ var context = new ExportContext
+ {
+ ActiveDocument = doc,
+ ViewFlipDecider = GetSelectedViewFlipDecider(),
+ FilenameTemplate = template,
+ OutputFolder = outputFolder,
+ CancellationToken = _cancellationTokenSource.Token,
+ ProgressCallback = (msg, level, file) => AddLogEvent(msg, level, file),
+ BomItemCallback = bomItem => AddBomItem(bomItem),
+ SolidWorksApp = _solidWorksService.Application
+ };
+
+ await _exportService.ExportAsync(context);
+}
+```
+
+- [ ] **Step 5: Remove all API/DB dropdown loading methods**
+
+Delete `LoadDrawingDropdownsAsync`, `UpdateDrawingDropdownAsync`, `LookupDrawingInfoFromHistoryAsync`, and any equipment/drawing dropdown event handlers.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add -A
+git commit -m "refactor: replace equipment/drawing dropdowns with filename template textbox"
+```
+
+---
+
+### Task 11: Update Program.cs and wire dependencies
+
+**Files:**
+- Modify: `ExportDXF/Program.cs`
+
+- [ ] **Step 1: Rewrite ServiceContainer**
+
+```csharp
+public class ServiceContainer
+{
+ public MainForm ResolveMainForm()
+ {
+ var swService = new SolidWorksService();
+ var bomExtractor = new BomExtractor();
+ var partExporter = new PartExporter();
+ var drawingExporter = new DrawingExporter();
+ var excelExportService = new ExcelExportService();
+ var logFileService = new LogFileService();
+
+ var exportService = new DxfExportService(
+ partExporter,
+ drawingExporter,
+ bomExtractor,
+ excelExportService,
+ logFileService);
+
+ var extractors = new IDrawingInfoExtractor[]
+ {
+ new EquipmentDrawingInfoExtractor(),
+ new DefaultDrawingInfoExtractor()
+ };
+
+ return new MainForm(swService, exportService, extractors);
+ }
+}
+```
+
+- [ ] **Step 2: Remove HttpClient, API client, DB context factory creation**
+
+Delete all code related to `HttpClient`, `FabWorksApiClient`, `FabWorksApiUrl`, `ExportDxfDbContext`, and `FileExportService` from Program.cs.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add ExportDXF/Program.cs
+git commit -m "refactor: rewire Program.cs DI — remove API/DB, add Excel and log services"
+```
+
+---
+
+### Task 12: Build, fix compile errors, and verify
+
+**Files:**
+- Various (fixing any remaining compile errors)
+
+- [ ] **Step 1: Build the solution**
+
+```bash
+cd ExportDXF
+dotnet build ExportDXF.csproj
+```
+
+- [ ] **Step 2: Fix any remaining compile errors**
+
+Common issues to watch for:
+- Remaining references to `IFabWorksApiClient`, `FabWorksApiClient`, `ApiExportDetail`, etc.
+- Remaining references to `ExportRecord`, `ExportDxfDbContext`
+- Missing `using` statements for new namespaces
+- `ExportContext` property name changes (`FilePrefix` → `FilenameTemplate`, `Equipment` → removed, etc.)
+- `DxfExportService` method signature changes
+
+- [ ] **Step 3: Clean build verification**
+
+```bash
+dotnet build ExportDXF.csproj --no-incremental
+```
+
+Expected: `Build succeeded. 0 Warning(s) 0 Error(s)`
+
+- [ ] **Step 4: Final commit**
+
+```bash
+git add -A
+git commit -m "fix: resolve remaining compile errors after API removal"
+```
+
+- [ ] **Step 5: Push to remote**
+
+```bash
+git push origin master
+```