diff --git a/.gitignore b/.gitignore
index bccd75e..00902a3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -245,3 +245,6 @@ ModelManifest.xml
# Test documents
TestDocs/
+
+# Superpowers specs and plans
+docs/superpowers/
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
deleted file mode 100644
index a6c6046..0000000
--- a/docs/superpowers/plans/2026-04-13-remove-api-excel-export.md
+++ /dev/null
@@ -1,1150 +0,0 @@
-# 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
-```
diff --git a/docs/superpowers/specs/2026-04-13-remove-api-excel-export-design.md b/docs/superpowers/specs/2026-04-13-remove-api-excel-export-design.md
deleted file mode 100644
index 2f2cf34..0000000
--- a/docs/superpowers/specs/2026-04-13-remove-api-excel-export-design.md
+++ /dev/null
@@ -1,181 +0,0 @@
-# ExportDXF: Remove API, Export to Excel
-
-**Date:** 2026-04-13
-**Status:** Draft
-
-## Goal
-
-Remove the FabWorks API and database dependencies from ExportDXF. Replace with local file output: DXF files + an Excel workbook (BOM + Cut Templates) that downstream tools like OpenNest can import directly. Make the tool self-contained — no server setup required.
-
-## Approach
-
-Merge the `feature/fabworks-api` branch into `master` to get its structural improvements (service layer, models, content hashing, EtchBendLines fixes), then remove the API/DB layer and replace with Excel + log file output.
-
----
-
-## Output Structure
-
-### Output Folder
-
-- Default location: `Templates/` folder in the same directory as the source SolidWorks file
-- If `Templates/` already exists, the export writes into it (see Revision Handling below)
-
-### Files Produced
-
-```
-C:\Projects\4321\
-├── 4321 A01.SLDDRW (source drawing)
-└── Templates\
- ├── 4321 A01 PT01.dxf
- ├── 4321 A01 PT02.dxf
- ├── 4321 A01 PT03.dxf
- ├── 4321 A01 PT03 Rev2.dxf (revised file, original kept)
- ├── 4321 A01.xlsx
- └── 4321 A01.log
-```
-
-### Excel Workbook
-
-Written with **ClosedXML 0.104.2** (same version as OpenNest).
-
-**"BOM" sheet** — exact copy of the SolidWorks BOM table. All visible columns and rows reproduced as-is. Only present when exporting from a Drawing that contains a BOM table. Part and Assembly exports omit this sheet.
-
-**"Cut Templates" sheet** — one row per sheet metal part:
-
-| Column | Description |
-|--------|-------------|
-| Item # | Item number from the BOM or export sequence |
-| File Name | DXF filename stem (no path, no extension) |
-| Revision | Revision number (1 for initial, increments on change) |
-| Thickness | Sheet metal thickness |
-| K-Factor | K-Factor value |
-| Bend Radius | Default bend radius |
-| Content Hash | SHA256 of DXF content (excluding HEADER section) |
-
-Present for all export types (Part, Assembly, Drawing).
-
-### Log Files
-
-**Per-export log:** `Templates/{prefix}.log`
-- Timestamped lines: `2026-04-13 14:32:05 [INFO] Exported 4321 A01 PT03.dxf`
-- Levels: INFO, WARNING, ERROR
-- Appends on re-export
-
-**App-level log:** `C:\ExportDXF\ExportDXF.log`
-- Rolling log capturing all exports across all drawings
-- Same format as per-export log
-
----
-
-## Filename Format Template
-
-### User Interface
-
-A single text box replaces the equipment/drawing dropdowns. The user types a format string that controls DXF and xlsx naming.
-
-**Example:** `4321 A01 PT{item_no:2}`
-
-### Supported Placeholders
-
-| Placeholder | Description | Example Output |
-|-------------|-------------|----------------|
-| `{item_no:N}` | Item number, zero-padded to N digits | `{item_no:2}` → `03` |
-| `{part_name}` | SolidWorks part name | `Bracket` |
-| `{config}` | Configuration name | `Default` |
-| `{material}` | Material name | `AISI 304` |
-
-### Naming Rules
-
-- DXF filename: full template evaluated per part → `4321 A01 PT03.dxf`
-- Excel filename: literal text before the first placeholder → `4321 A01.xlsx`
-- Log filename: same stem as excel → `4321 A01.log`
-- Validation: template must contain `{item_no}` (or `{item_no:N}`) to avoid filename collisions
-- If the template starts with a placeholder (no literal prefix), fall back to the document name for the xlsx/log filename
-
-### Auto-Fill
-
-On document open, the format template text box is auto-filled via `IDrawingInfoExtractor`.
-
----
-
-## Drawing Info Extraction
-
-### Interface
-
-```csharp
-public interface IDrawingInfoExtractor
-{
- bool TryExtract(string documentName, out DrawingInfo info);
-}
-
-public class DrawingInfo
-{
- public string EquipmentNumber { get; set; }
- public string DrawingNumber { get; set; }
- public string DefaultTemplate { get; set; }
-}
-```
-
-### Implementations
-
-**`DefaultDrawingInfoExtractor`** — uses the document name as literal prefix, appends the default suffix from app.config (`DefaultSuffix` setting, default `PT{item_no:2}`).
-
-Example: document `MyPart.SLDPRT` → template `MyPart {item_no:2}` (or with configured suffix)
-
-**`EquipmentDrawingInfoExtractor`** — parses the `{EquipmentNo} {DrawingNo}` pattern from document names in AJ's workplace format. Builds template like `4321 A01 PT{item_no:2}`.
-
-### Resolution
-
-Extractors are tried in order. First one that returns `true` wins. Falls back to `DefaultDrawingInfoExtractor`.
-
----
-
-## Revision Handling
-
-When re-exporting to an existing `Templates` folder:
-
-1. Read the existing xlsx Cut Templates sheet (if present)
-2. For each part being exported, compute the DXF content hash
-3. **Hash matches existing row** → skip, leave existing DXF file untouched
-4. **Hash differs from existing row** → write new DXF with revision suffix (e.g., `PT03 Rev2.dxf`), update the xlsx row with new filename, new hash, incremented revision number
-5. **New part (no existing row)** → write DXF, add new row to xlsx with revision 1
-6. Old revision DXF files are kept in the folder as history
-
-The BOM sheet (if present) is fully rewritten from the current drawing's BOM table on each export.
-
----
-
-## What We Keep from feature/fabworks-api
-
-- Service layer architecture: `DxfExportService`, `IPartExporter`, `IDrawingExporter`, `IBomExtractor`
-- Models: `BomItem`, `CutTemplate`, `Item`, `ExportContext`, `LogEvent`
-- `ContentHasher` utility (SHA256 excluding DXF HEADER section)
-- All 9 EtchBendLines submodule fixes (ACadSharp migration, bend detection, degree symbol, etc.)
-- Async export flow with cancellation
-- View flip decider
-- SolidWorks user input disabling during export
-
-## What We Remove
-
-- `FabWorksApiClient` and all API DTOs
-- `ExportDxfDbContext`, EF Core migrations, SQL Server connection string
-- NuGet packages: `Microsoft.EntityFrameworkCore.SqlServer`, `Microsoft.EntityFrameworkCore.Tools`
-- Equipment/Drawing dropdowns from MainForm
-- Any FabWorks.Core / FabWorks.Api projects (if present after merge)
-
-## What We Add
-
-- `IDrawingInfoExtractor` interface + two implementations
-- Format template text box with placeholder parsing
-- `ExcelExportService` using ClosedXML — reads/writes BOM + Cut Templates sheets
-- `LogFileService` — per-export and app-level log writing
-- Output folder logic (Templates folder next to source, revision handling)
-- NuGet package: `ClosedXML 0.104.2`
-- `DefaultSuffix` setting in app.config
-
-## UI Changes
-
-- Replace equipment/drawing combo boxes with a single format template text box
-- Keep 3 tabs: Log Events, Bill of Materials, Cut Templates
-- Keep Start/Stop button
-- Keep View Flip Decider dropdown