Files
44-Scale/docs/superpowers/plans/2026-07-28-e440-arc-dial.md
T

49 KiB
Raw Blame History

E440 Arc-Dial Scale Generator 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: Replace the M1730 linear-strip scale generator with an E440-style round-dial (arc) scale generator, matching the hand-drawn reference e440.svg, across the web app, the three deliverable SVG files, and all docs/infra naming.

Architecture: web/app.py's generate_svg() is rewritten around polar geometry (a fixed pivot point, fixed rim radius, fixed angular sweep) instead of the old Cartesian geometry (fixed x-range, fixed y-baseline). Ticks and labels are drawn as <line>/<text>/path elements positioned "as if angle=0" (straight up from the pivot) and then given an SVG rotate(angle, PIVOT_X, PIVOT_Y) transform — this reuses the reference file's own rotation trick but around an explicit pivot, which avoids inverse-rotation math. All four scale mappings (linear/√/log/1÷x), both output modes (paths/text), custom labels, and unit markup are ported by substituting angle for x. range_label is replaced by independent left_label/right_label fields (each up to 2 lines). The three deliverable SVGs, README, CLAUDE.md, Dockerfile-adjacent docs, and k8s manifests are all renamed/rewritten from M1730 to E440.

Tech Stack: Python 3 / Flask, fontTools (TTFont, SVGPathPen) for glyph-to-path conversion — both already in web/requirements.txt, no new dependencies.

Global Constraints

  • No build, lint, or test tooling exists in this repo (per CLAUDE.md) — do not add pytest or any test framework. Verification steps in this plan use plain python3 -c / heredoc scripts with assert, run directly via Bash.
  • All canvas/geometry constants below are measured directly from e440.svg (or computed from measurements) — use these exact values, don't re-derive or approximate differently.
  • Coordinate convention: all constants are expressed in the pre-layer-translate local frame (the same frame e440.svg's own elements use), with the SVG output wrapped in <g transform="translate(-60.717741,-107.14126)">. This mirrors the existing M1730 code's convention (ZERO_X, MAX_X, etc. were also pre-translate).
  • Canvas size is fixed: width="93.824249mm" height="57.215687mm" viewBox="0 0 93.824249 57.215687".
  • Colors/strokes: ink #1a1a1a for ticks/labels/unit/corner text (matches reference), body fill #f9f9f9 with #1a1a1a stroke at width 0.184, pivot guide circle stroke #b3b3b3 at width 0.184, rim arc stroke #000000 at width 0.484.
  • Geometry constants (local frame, mm):
    • PIVOT_X = 107.42845, PIVOT_Y = 162.62819
    • R0 = 43.842751 (rim / inner-tick radius)
    • ANGLE_MIN = -45.667193, ANGLE_MAX = 45.667193 (degrees from vertical, clockwise-positive; low value on the left/negative side)
    • MAJOR_LEN = 4.7839203, MAJOR_W = 0.79658329
    • MEDIUM_LEN = 3.1215858, MEDIUM_W = 0.37985462
    • SMALL_LEN = 1.8121802, SMALL_W = 0.27478597
    • LABEL_RADIUS = 49.862
    • UNIT_X = 107.03512, UNIT_Y = 136.64594
    • LEFT_X = 73.432251, LEFT_Y0 = 145.16051
    • RIGHT_X = 142.4485, RIGHT_Y0 = 145.32773
    • CORNER_LINE_SPACING = 4.4586, CORNER_FONT_SIZE = 3.56685
    • PIVOT_CIRCLE_R = 1.7287594
  • The half_height_unit feature (and its UI toggle) is dropped — it existed only to solve a crowding problem specific to M1730's narrow side panel, which doesn't exist in the E440 layout.
  • Point-on-circle formula (verified against the reference to within 0.003mm): x = PIVOT_X + radius * sin(radians(angle_deg)), y = PIVOT_Y - radius * cos(radians(angle_deg)).

Task 1: Clean up fonts directory and register the DIN font labels/default

Files:

  • Modify: fonts/ (remove stray Windows metadata files)
  • Modify: web/app.py:19-26 (_FONT_LABELS dict), web/app.py:51 (_DEFAULT_FONT)

Interfaces:

  • Consumes: nothing new — _load_fonts() (web/app.py:29-47) already globs *.ttf in fonts/ and builds _FontData per file; unchanged.

  • Produces: _DEFAULT_FONT now resolves to the DIN regular font's key, used by every later task's default font_key argument.

  • Step 1: Remove stray Zone.Identifier files

rm -f "fonts/alte-din-1451-mittelschrift.regular.ttf:Zone.Identifier" \
      "fonts/alte-din-1451-mittelschrift.gepraegt.ttf:Zone.Identifier"
ls fonts/

Expected: only .ttf files remain (6 GOST-family + 2 DIN files).

  • Step 2: Install defusedxml into the venv for later verification steps

This plan's well-formedness checks (Tasks 2 and 9) parse generated SVGs with defusedxml instead of the stdlib xml.etree.ElementTree, per this repo's security guidance (stdlib XML parsers are vulnerable to XXE/billion-laughs by default). It's not an app.py runtime dependency, just a verification-script tool, so it's installed into the venv but not added to web/requirements.txt.

cd /home/adebaumann/development/44-Scale
source .venv/bin/activate
pip install defusedxml
python3 -c "import defusedxml; print('OK')"

Expected: OK.

  • Step 3: Add DIN font display labels and change the default font

In web/app.py, in the _FONT_LABELS dict, add two entries (key = filename stem, i.e. Path('alte-din-1451-mittelschrift.regular.ttf').stem):

_FONT_LABELS = {
    'GOST_2.30481_type_A':                    'GOST Type A',
    'GOST_2.30481_type_A_Bold':                'GOST Type A Bold',
    'Gost2.30481TypeB':                        'GOST Type B',
    'ISOCPEUR':                                'ISOCPEUR',
    'SIEMENS_GOST_Type_A':                     'Siemens GOST Type A',
    'TGL_31034-1':                              'TGL 31 034',
    'alte-din-1451-mittelschrift.regular':      'Alte DIN 1451 Mittelschrift',
    'alte-din-1451-mittelschrift.gepraegt':     'Alte DIN 1451 Mittelschrift (Gepraegt)',
}

Change the default font constant:

_DEFAULT_FONT = 'alte-din-1451-mittelschrift.regular'
  • Step 4: Verify the font loads and is the default
cd /home/adebaumann/development/44-Scale
python3 -c "
import sys; sys.path.insert(0, 'web')
from app import _FONTS, _DEFAULT_FONT
assert _DEFAULT_FONT in _FONTS, _FONTS.keys()
assert _FONTS[_DEFAULT_FONT].label == 'Alte DIN 1451 Mittelschrift'
print('OK', len(_FONTS), 'fonts loaded')
"

Expected: OK 8 fonts loaded.

  • Step 5: Commit
git add fonts/ web/app.py
git commit -m "Add DIN 1451 fonts as default; drop stray Zone.Identifier files"

Task 2: Rewrite generate_svg() skeleton around arc geometry (body + pivot + rim, no ticks/labels yet)

Files:

  • Modify: web/app.py (constants block web/app.py:53-77, generate_svg() web/app.py:320-488, route handler web/app.py:498-523)

Interfaces:

  • Consumes: _FONTS, _DEFAULT_FONT, _markup_unit_svg, _text_to_paths, _fmt (all unchanged, from earlier in the file).

  • Produces: new generate_svg(unit, min_val, max_val, left_label, right_label, big_ticks, small_ticks, label_count, use_paths=True, scale_type='linear', custom_labels=None, label_size=5.1126, font_key=None) signature that later tasks extend. Produces _point_at(radius, angle_deg) helper used by Tasks 34.

  • Step 1: Replace the linear geometry constants with arc constants

Replace web/app.py:53-77 (from # SVG absolute coordinate constants... through the _UNIT_BOX_* block) with:

# SVG absolute coordinate constants (1 user unit = 1 mm), pre-layer-translate
# local frame — the <g> wrapper applies translate(-60.717741,-107.14126).
PIVOT_X, PIVOT_Y = 107.42845, 162.62819
R0 = 43.842751          # rim / inner-tick radius
ANGLE_MIN, ANGLE_MAX = -45.667193, 45.667193   # degrees from vertical, clockwise-positive

MAJOR_LEN,  MAJOR_W  = 4.7839203, 0.79658329
MEDIUM_LEN, MEDIUM_W = 3.1215858, 0.37985462
SMALL_LEN,  SMALL_W  = 1.8121802, 0.27478597

LABEL_RADIUS = 49.862

UNIT_X, UNIT_Y = 107.03512, 136.64594

LEFT_X, LEFT_Y0   = 73.432251, 145.16051
RIGHT_X, RIGHT_Y0 = 142.4485, 145.32773
CORNER_LINE_SPACING = 4.4586
CORNER_FONT_SIZE = 3.56685

PIVOT_CIRCLE_R = 1.7287594

BODY_D = (
    "m 62.162097,107.14126 c -0.800087,0 -1.444356,0.64428 -1.444356,1.44436 v 45.44839 "
    "c 0,0.80008 0.644269,1.44436 1.444356,1.44436 h 7.345288 c 0.902634,0 1.233517,1.12706 "
    "1.233517,1.12706 l 0.09043,3.03909 21.903573,-0.0904 2.226221,-5.98775 c 8.352594,-2.8884 "
    "16.624974,-2.8884 24.817094,0 l 2.22674,5.98775 21.90357,0.0904 0.0904,-3.03909 c 0,0 "
    "0.33088,-1.12706 1.23352,-1.12706 h 7.86567 c 0.80009,0 1.44384,-0.64428 1.44384,-1.44436 "
    "v -45.44839 c 0,-0.80008 -0.64375,-1.44436 -1.44384,-1.44436 z m 29.619898,45.85354 "
    "a 1.3990685,1.3990685 0 0 1 1.398881,1.39939 1.3990685,1.3990685 0 0 1 -1.398881,1.39888 "
    "1.3990685,1.3990685 0 0 1 -1.399398,-1.39888 1.3990685,1.3990685 0 0 1 1.399398,-1.39939 "
    "z m 31.220835,0 a 1.3990685,1.3990685 0 0 1 1.39888,1.39939 1.3990685,1.3990685 0 0 1 "
    "-1.39888,1.39888 1.3990685,1.3990685 0 0 1 -1.3994,-1.39888 1.3990685,1.3990685 0 0 1 "
    "1.3994,-1.39939 z"
)

_UNIT_BOX_X0, _UNIT_BOX_X1 = UNIT_X - 15.0, UNIT_X + 15.0
_UNIT_BOX_Y0, _UNIT_BOX_Y1 = UNIT_Y - 6.0, UNIT_Y + 6.0
_UNIT_MAX_FS = 9.46938021
_UNIT_SCALE_X = 1.0


def _point_at(radius: float, angle_deg: float):
    th = math.radians(angle_deg)
    return PIVOT_X + radius * math.sin(th), PIVOT_Y - radius * math.cos(th)
  • Step 2: Simplify _markup_unit_svg to drop the half-height box-override params

Replace the _markup_unit_svg function (originally web/app.py:248-268) with:

def _markup_unit_svg(unit: str, fd: _FontData, fill='#1a1a1a') -> str:
    nodes = _parse_markup(unit)
    x0, x1 = _UNIT_BOX_X0, _UNIT_BOX_X1
    y0, y1 = _UNIT_BOX_Y0, _UNIT_BOX_Y1
    xc, yc = (x0 + x1) / 2, (y0 + y1) / 2

    w1 = _measure(nodes, 1.0, fd)
    top1, bot1 = _vbounds(nodes, 1.0, fd)
    fs = _UNIT_MAX_FS
    if w1 > 0:
        fs = min(fs, (x1 - x0) / (w1 * _UNIT_SCALE_X))
    if bot1 - top1 > 0:
        fs = min(fs, (y1 - y0) / (bot1 - top1))

    w = _measure(nodes, fs, fd)
    top, bot = _vbounds(nodes, fs, fd)
    y_base = yc - (top + bot) / 2
    parts, _ = _draw_nodes(nodes, xc - w / 2, y_base, fs, fill, fd)
    cond = f'translate({xc:.5f},0) scale({_UNIT_SCALE_X},1) translate({-xc:.5f},0)'
    return f'<g transform="{cond}">\n{chr(10).join(parts)}\n</g>'

(This drops the box_y0/box_y1 parameters and the module-level _UNIT_BOX_HALF_Y0 constant — search web/app.py for _UNIT_BOX_HALF_Y0 and half_height_unit and delete every reference; they no longer exist anywhere after this task.)

  • Step 3: Delete the now-unused _unit_font_size function

Delete the _unit_font_size function entirely (originally web/app.py:271-275) — after Task 5 the unit is always rendered via _markup_unit_svg, so this fast-path helper becomes dead code.

  • Step 4: Replace generate_svg() with the new skeleton

Replace the whole generate_svg function (originally web/app.py:320-488) with:

def generate_svg(unit, min_val, max_val, left_label, right_label, big_ticks, small_ticks,
                 label_count, use_paths=True, scale_type='linear', custom_labels=None,
                 label_size=5.1126, font_key=None):
    fd = _FONTS.get(font_key) or _FONTS.get(_DEFAULT_FONT) or next(iter(_FONTS.values()))

    font_face = (
        '<defs><style>'
        f'@font-face {{font-family:"{fd.label}";'
        f'src:url("data:font/truetype;base64,{fd.b64}") format("truetype");}}'
        '</style></defs>'
    )

    parts = [
        '<?xml version="1.0" encoding="UTF-8" standalone="no"?>',
        '<svg width="93.824249mm" height="57.215687mm"'
        ' viewBox="0 0 93.824249 57.215687" version="1.1"'
        ' xmlns="http://www.w3.org/2000/svg">',
    ]

    if use_paths:
        parts.append('  <g transform="translate(-60.717741,-107.14126)">')
    else:
        parts += [font_face, '  <g transform="translate(-60.717741,-107.14126)">']

    parts.append(
        f'    <path style="fill:#f9f9f9;stroke:#1a1a1a;stroke-width:0.184;'
        f'stroke-linecap:round;stroke-miterlimit:10" d="{BODY_D}" />'
    )

    p0 = _point_at(R0, ANGLE_MIN)
    pmid = _point_at(R0, 0.0)
    p1 = _point_at(R0, ANGLE_MAX)
    parts.append(
        f'    <path style="fill:none;stroke:#000000;stroke-width:0.484;'
        f'stroke-linecap:round;stroke-miterlimit:10"'
        f' d="M {p0[0]:.5f},{p0[1]:.5f} A {R0},{R0} 0 0 1 {pmid[0]:.5f},{pmid[1]:.5f}'
        f' A {R0},{R0} 0 0 1 {p1[0]:.5f},{p1[1]:.5f}" />'
    )

    parts.append(
        f'    <circle style="fill:none;stroke:#b3b3b3;stroke-width:0.184;'
        f'stroke-linecap:round;stroke-miterlimit:10"'
        f' cx="{PIVOT_X:.5f}" cy="{PIVOT_Y:.5f}" r="{PIVOT_CIRCLE_R}" />'
    )

    parts += ['  </g>', '</svg>']
    return '\n'.join(parts)

(Ticks, labels, unit, and corner text are added by Tasks 36; this step must produce a valid, renderable SVG containing just the case body, rim arc, and pivot guide circle.)

  • Step 5: Update the /generate Flask route to the new parameter set

Replace the /generate route body (originally web/app.py:498-523) with:

@app.route('/generate', methods=['POST'])
def generate():
    unit        = request.form.get('unit', '%')
    min_val     = float(request.form.get('min', 0))
    max_val     = float(request.form.get('max', 100))
    left_label  = request.form.get('left_label', '')
    right_label = request.form.get('right_label', '')
    big_ticks   = max(1, min(50, int(request.form.get('big_ticks', 10))))
    small_ticks = max(0, min(20, int(request.form.get('small_ticks', 4))))
    label_count = max(2, int(request.form.get('label_count', 6)))
    use_paths   = request.form.get('output_mode', 'paths') == 'paths'
    scale_type  = request.form.get('scale_type', 'linear')
    font_key    = request.form.get('font', _DEFAULT_FONT)
    if request.form.get('max_is_inf') == '1':
        max_val = math.inf

    custom_labels = [s.strip() for s in request.form.get('labels', '').split(',') if s.strip()] or None
    label_size    = max(0.5, min(25.0, float(request.form.get('label_size', 5.1126))))

    try:
        svg = generate_svg(unit, min_val, max_val, left_label, right_label, big_ticks,
                           small_ticks, label_count, use_paths, scale_type, custom_labels,
                           label_size, font_key)
    except ValueError as err:
        return Response(str(err), status=400, mimetype='text/plain')
    return Response(svg, mimetype='image/svg+xml')
  • Step 6: Verify the skeleton renders a valid SVG
cd /home/adebaumann/development/44-Scale
python3 -c "
import sys; sys.path.insert(0, 'web')
from app import generate_svg
svg = generate_svg('uA', 0, 50, 'Text\nLeft', 'Text\nRight', 5, 9, 6)
assert svg.startswith('<?xml')
assert 'viewBox=\"0 0 93.824249 57.215687\"' in svg
assert 'translate(-60.717741,-107.14126)' in svg
assert 'cx=\"107.42845\"' in svg
from defusedxml import ElementTree as ET
ET.fromstring(svg)
print('OK, valid XML,', len(svg), 'bytes')
"

Expected: OK, valid XML, ... bytes with no exception. (Requires defusedxmlpip install defusedxml if not already available.)

  • Step 7: Commit
git add web/app.py
git commit -m "Rewrite generate_svg skeleton around arc geometry (body/rim/pivot only)"

Task 3: Implement tick generation (3-tier, all 4 scale mappings)

Files:

  • Modify: web/app.py (inside generate_svg, after the pivot-circle parts.append from Task 2 Step 4, before parts += [' </g>', '</svg>'])

Interfaces:

  • Consumes: _point_at, R0, ANGLE_MIN, ANGLE_MAX, MAJOR_LEN/W, MEDIUM_LEN/W, SMALL_LEN/W from Task 2.

  • Produces: big_angles (list[float], length big_ticks + 1) and big_vals (list[float], same length) — consumed by Task 4's label loop.

  • Step 1: Add the mapping functions (value_to_angle / major_value) and tick-line helper

Insert immediately after the fd = _FONTS.get(...) line inside generate_svg:

    if scale_type == 'log':
        if min_val <= 0 or max_val <= 0:
            raise ValueError("Logarithmic scale requires min and max values greater than 0.")
        lmin, lmax = math.log(min_val), math.log(max_val)
        lspan = lmax - lmin
        if lspan == 0:
            raise ValueError("Logarithmic scale requires min and max values to differ.")

        def value_to_angle(v):
            return ANGLE_MIN + (ANGLE_MAX - ANGLE_MIN) * (math.log(v) - lmin) / lspan

        def major_value(i):
            return min_val * (max_val / min_val) ** (i / big_ticks)

    elif scale_type == 'sqrt':
        if min_val < 0:
            raise ValueError("Square-root scale requires min value ≥ 0.")
        span = max_val - min_val
        if span <= 0:
            raise ValueError("Square-root scale requires max value greater than min.")

        def value_to_angle(v):
            return ANGLE_MIN + (ANGLE_MAX - ANGLE_MIN) * math.sqrt((v - min_val) / span)

        def major_value(i):
            return min_val + span * (i / big_ticks) ** 2

    elif scale_type == 'reciprocal':
        if min_val <= 0:
            raise ValueError("Reciprocal scale requires min and max values greater than 0.")
        if not math.isinf(max_val) and max_val <= min_val:
            raise ValueError("Reciprocal scale requires max value greater than min.")
        if math.isinf(max_val):
            def value_to_angle(v):
                if math.isinf(v):
                    return ANGLE_MIN
                return ANGLE_MIN + (ANGLE_MAX - ANGLE_MIN) * min_val / v

            def major_value(i):
                if i == 0:
                    return math.inf
                return min_val * big_ticks / i
        else:
            if max_val <= 0:
                raise ValueError("Reciprocal scale requires min and max values greater than 0.")
            k = 1.0 / min_val - 1.0 / max_val

            def value_to_angle(v):
                return ANGLE_MIN + (ANGLE_MAX - ANGLE_MIN) * (1.0 / v - 1.0 / max_val) / k

            def major_value(i):
                return 1.0 / (1.0 / max_val + i / big_ticks * k)

    else:
        span = max_val - min_val

        def value_to_angle(v):
            if span == 0:
                return ANGLE_MIN
            return ANGLE_MIN + (ANGLE_MAX - ANGLE_MIN) * (v - min_val) / span

        def major_value(i):
            return min_val + span * i / big_ticks

    big_interval_angle = (ANGLE_MAX - ANGLE_MIN) / big_ticks
    big_angles = [ANGLE_MIN + i * big_interval_angle for i in range(big_ticks + 1)]
    big_vals = [major_value(i) for i in range(big_ticks + 1)]

    if custom_labels:
        label_count = max(1, min(len(custom_labels), big_ticks + 1))
        custom_labels = custom_labels[:label_count]
    else:
        label_count = max(2, min(label_count, big_ticks + 1))

    if label_count == 1:
        label_indices = [0]
    else:
        label_indices = [round(k * big_ticks / (label_count - 1)) for k in range(label_count)]

    def _tick_line(angle_deg, length, width):
        x = PIVOT_X
        y1 = PIVOT_Y - R0
        y2 = PIVOT_Y - R0 - length
        return (
            f'    <line x1="{x:.5f}" y1="{y1:.5f}" x2="{x:.5f}" y2="{y2:.5f}"'
            f' style="stroke:#1a1a1a;stroke-width:{width:.5f};stroke-linecap:butt"'
            f' transform="rotate({angle_deg:.5f},{PIVOT_X:.5f},{PIVOT_Y:.5f})" />'
        )
  • Step 2: Draw the ticks

Insert immediately after the pivot-circle parts.append(...) block (still before parts += [' </g>', '</svg>']):

    for angle in big_angles:
        parts.append(_tick_line(angle, MAJOR_LEN, MAJOR_W))

    if small_ticks > 0:
        sub = small_ticks + 1
        medium_j = sub // 2 if small_ticks % 2 == 1 else None
        for i in range(big_ticks):
            for j in range(1, small_ticks + 1):
                if scale_type == 'sqrt':
                    angle = big_angles[i] + (j / sub) ** 2 * (big_angles[i + 1] - big_angles[i])
                elif scale_type == 'reciprocal' or math.isinf(big_vals[i]):
                    angle = big_angles[i] + (j / sub) * (big_angles[i + 1] - big_angles[i])
                else:
                    v = big_vals[i] + j * (big_vals[i + 1] - big_vals[i]) / sub
                    angle = value_to_angle(v)
                length, width = (MEDIUM_LEN, MEDIUM_W) if j == medium_j else (SMALL_LEN, SMALL_W)
                parts.append(_tick_line(angle, length, width))
  • Step 3: Verify tick angles for the reference case (050, 5 major, 9 minor)
cd /home/adebaumann/development/44-Scale
python3 -c "
import sys; sys.path.insert(0, 'web')
from app import generate_svg
svg = generate_svg('uA', 0, 50, '', '', 5, 9, 6)
# major ticks at -45.66719, -27.40032, -9.13344, 9.13344, 27.40032, 45.66719
for a in ['-45.66719', '-27.40032', '-9.13344', '9.13344', '27.40032', '45.66719']:
    assert f'rotate({a},' in svg, a
# medium ticks (values 5,15,25,35,45) at the interval midpoints
for a in ['-36.53375', '-18.26688', '0.00000', '18.26688', '36.53375']:
    assert f'rotate({a},' in svg, a
assert svg.count('<line') == 6 + 5 + 40   # 6 major + 5 medium + 40 small (8 per interval * 5)
print('OK')
"

Expected: OK with no assertion error.

  • Step 4: Commit
git add web/app.py
git commit -m "Implement arc tick generation with 3 tiers across all scale mappings"

Task 4: Implement numeric label generation

Files:

  • Modify: web/app.py (inside generate_svg, after the tick-drawing block from Task 3, before parts += [' </g>', '</svg>'])

Interfaces:

  • Consumes: big_angles, big_vals, label_indices, custom_labels, label_size, _point_at, LABEL_RADIUS, _text_to_paths, _fmt — all already available inside generate_svg.

  • Produces: nothing new consumed by later tasks (labels are a leaf feature).

  • Step 1: Draw the labels

Insert immediately after the tick small-ticks loop from Task 3 Step 2:

    lx, ly = PIVOT_X, PIVOT_Y - LABEL_RADIUS
    for k, idx in enumerate(label_indices):
        angle = big_angles[idx]
        label = custom_labels[k] if custom_labels else _fmt(big_vals[idx])
        wrap = f'rotate({angle:.5f},{PIVOT_X:.5f},{PIVOT_Y:.5f})'
        if use_paths:
            parts.append('    ' + _text_to_paths(label, lx, ly, label_size, fd,
                                                  anchor='middle', wrap_transform=wrap))
        else:
            parts.append(
                f'    <text style="font-size:{label_size:.5f}px;font-family:\'{fd.label}\',monospace;fill:#1a1a1a"'
                f' text-anchor="middle" x="{lx:.5f}" y="{ly:.5f}" transform="{wrap}">{escape(label)}</text>'
            )
  • Step 2: Verify labels for the reference case
cd /home/adebaumann/development/44-Scale
python3 -c "
import sys; sys.path.insert(0, 'web')
from app import generate_svg
svg = generate_svg('uA', 0, 50, '', '', 5, 9, 6, use_paths=False)
for val, angle in [('0','-45.66719'), ('10','-27.40032'), ('20','-9.13344'),
                    ('30','9.13344'), ('40','27.40032'), ('50','45.66719')]:
    assert f'rotate({angle},' in svg and f'>{val}</text>' in svg, (val, angle)
print('OK')

svg2 = generate_svg('uA', 0, 50, '', '', 5, 9, 3, custom_labels=['Lo','Mid','Hi'], use_paths=False)
for val in ('Lo', 'Mid', 'Hi'):
    assert f'>{val}</text>' in svg2, val
print('OK custom labels')
"

Expected: OK then OK custom labels.

  • Step 3: Commit
git add web/app.py
git commit -m "Implement rotated numeric label generation for the arc scale"

Task 5: Implement the unit label

Files:

  • Modify: web/app.py (inside generate_svg, after the label loop from Task 4)

Interfaces:

  • Consumes: _markup_unit_svg (Task 2 Step 2), unit, fd.

  • Produces: nothing new consumed later.

  • Step 1: Draw the unit label

Insert immediately after the label loop from Task 4 Step 1:

    parts.append('    ' + _markup_unit_svg(unit, fd))
  • Step 2: Verify plain and markup units render
cd /home/adebaumann/development/44-Scale
python3 -c "
import sys; sys.path.insert(0, 'web')
from app import generate_svg
svg = generate_svg('uA', 0, 50, '', '', 5, 9, 6)
assert svg.count('<path') >= 2   # body path + rim path + at least glyph paths
svg2 = generate_svg('cm^2', 0, 50, '', '', 5, 9, 6)
assert svg2 != svg
print('OK')
"

Expected: OK.

  • Step 3: Commit
git add web/app.py
git commit -m "Wire up unit label rendering via markup-aware helper"

Task 6: Implement left/right corner labels; fix _text_to_paths end-anchor support

Files:

  • Modify: web/app.py (_text_to_paths function, originally web/app.py:278-305; and inside generate_svg, after the unit label from Task 5)

Interfaces:

  • Consumes: left_label, right_label (strings, already threaded through the function signature and route since Task 2), LEFT_X/Y0, RIGHT_X/Y0, CORNER_LINE_SPACING, CORNER_FONT_SIZE.

  • Produces: nothing new — this is the last content block before </g></svg>.

  • Step 1: Add end anchor support to _text_to_paths

_text_to_paths currently only handles anchor == 'middle' for width-based offset (start is the implicit default with no offset). Right-anchored corner text needs a third case. Replace the anchor-handling block at the top of _text_to_paths:

    if anchor == 'middle':
        total_w = sum(
            fd.hmtx.get(fd.cmap.get(ord(ch)), (0, 0))[0] * s
            for ch in text if fd.cmap.get(ord(ch))
        )
        x -= total_w / 2

with:

    if anchor in ('middle', 'end'):
        total_w = sum(
            fd.hmtx.get(fd.cmap.get(ord(ch)), (0, 0))[0] * s
            for ch in text if fd.cmap.get(ord(ch))
        )
        x -= total_w / 2 if anchor == 'middle' else total_w
  • Step 2: Draw the corner labels

Insert immediately after the unit-label line from Task 5 Step 1:

    left_lines = left_label.split('\n')[:2]
    right_lines = right_label.split('\n')[:2]

    for i, line in enumerate(left_lines):
        if not line:
            continue
        y = LEFT_Y0 + i * CORNER_LINE_SPACING
        if use_paths:
            parts.append('    ' + _text_to_paths(line, LEFT_X, y, CORNER_FONT_SIZE, fd, anchor='start'))
        else:
            parts.append(
                f'    <text style="font-size:{CORNER_FONT_SIZE:.5f}px;font-family:\'{fd.label}\',monospace;fill:#1a1a1a"'
                f' x="{LEFT_X:.5f}" y="{y:.5f}">{escape(line)}</text>'
            )

    for i, line in enumerate(right_lines):
        if not line:
            continue
        y = RIGHT_Y0 + i * CORNER_LINE_SPACING
        if use_paths:
            parts.append('    ' + _text_to_paths(line, RIGHT_X, y, CORNER_FONT_SIZE, fd, anchor='end'))
        else:
            parts.append(
                f'    <text style="font-size:{CORNER_FONT_SIZE:.5f}px;font-family:\'{fd.label}\',monospace;fill:#1a1a1a"'
                f' text-anchor="end" x="{RIGHT_X:.5f}" y="{y:.5f}">{escape(line)}</text>'
            )
  • Step 3: Verify corner labels (2-line, start/end anchored) render correctly
cd /home/adebaumann/development/44-Scale
python3 -c "
import sys; sys.path.insert(0, 'web')
from app import generate_svg
svg = generate_svg('uA', 0, 50, 'Text\nLeft', 'Text\nRight', 5, 9, 6, use_paths=False)
assert '>Text</text>' in svg and '>Left</text>' in svg and '>Right</text>' in svg
assert 'text-anchor=\"end\"' in svg
svg_one_line = generate_svg('uA', 0, 50, 'OnlyLine', '', 5, 9, 6, use_paths=False)
assert '>OnlyLine</text>' in svg_one_line
print('OK')
"

Expected: OK.

  • Step 4: Commit
git add web/app.py
git commit -m "Implement left/right corner labels; add end-anchor support to text-to-paths"

Task 7: Update the web form (web/templates/index.html)

Files:

  • Modify: web/templates/index.html

Interfaces:

  • Consumes: the /generate route's new form fields (left_label, right_label) from Task 2 Step 5.

  • Produces: nothing consumed by later tasks — this is the UI leaf.

  • Step 1: Update page title and header wordmark

Replace:

  <title>M1730 Scale Generator — Baumann Works</title>

with:

  <title>E440 Scale Generator — Baumann Works</title>

Replace:

      <span class="wordmark-sub">M1730 Scale Generator</span>

with:

      <span class="wordmark-sub">E440 Scale Generator</span>
  • Step 2: Replace the Unit/Range-label row with Unit/Left-label/Right-label, dropping the half-height toggle

Replace the entire Labels section block:

      <div class="section">
        <div class="section-eye">Labels</div>
        <div class="row row-2">
          <div class="field">
            <label>Unit</label>
            <input name="unit" type="text" value="%" required>
            <span style="font-family:var(--f-mono);font-size:0.65rem;color:var(--muted)">Markup: cm^2 · H_2O · {1/2} fraction · nests, e.g. {m/s^2}</span>
            <div style="display:flex;align-items:center;gap:0.5rem;margin-top:0.2rem">
              <div class="mode-toggle" role="group" aria-label="Unit height">
                <button type="button" class="mode-btn active" data-unit-height="normal">Full height</button>
                <button type="button" class="mode-btn" data-unit-height="half"
                  title="Restrict unit label to the upper half of the panel — use when numeric labels visually crowd the unit">Half height</button>
              </div>
              <input type="hidden" name="half_height_unit" id="half-height-unit" value="0">
            </div>
          </div>
          <div class="field">
            <label>Range label</label>
            <input name="range_label" type="text" value="5mA" required>
          </div>
        </div>
      </div>

with:

      <div class="section">
        <div class="section-eye">Labels</div>
        <div class="row row-2">
          <div class="field">
            <label>Unit</label>
            <input name="unit" type="text" value="uA" required>
            <span style="font-family:var(--f-mono);font-size:0.65rem;color:var(--muted)">Markup: cm^2 · H_2O · {1/2} fraction · nests, e.g. {m/s^2}</span>
          </div>
        </div>
        <div class="row row-2" style="margin-top:1.25rem">
          <div class="field">
            <label>Left label (up to 2 lines)</label>
            <textarea name="left_label" rows="2" style="background:var(--white);border:1px solid var(--rule);border-radius:1px;padding:0.5rem 0.7rem;font-family:var(--f-body);font-size:0.925rem;color:var(--ink);width:100%;outline:none;resize:vertical">Text
Left</textarea>
          </div>
          <div class="field">
            <label>Right label (up to 2 lines)</label>
            <textarea name="right_label" rows="2" style="background:var(--white);border:1px solid var(--rule);border-radius:1px;padding:0.5rem 0.7rem;font-family:var(--f-body);font-size:0.925rem;color:var(--ink);width:100%;outline:none;resize:vertical">Text
Right</textarea>
          </div>
        </div>
      </div>
  • Step 3: Update the default Label size value to match the new reference default

Replace:

            <input name="label_size" type="number" step="any" min="0.5" max="25" value="9" required>

with:

            <input name="label_size" type="number" step="any" min="0.5" max="25" value="5.1126" required>
  • Step 4: Remove the now-dead unit-height JS handler and download filename

Delete this block from the <script>:

    document.querySelectorAll('[data-unit-height]').forEach(btn => {
      btn.addEventListener('click', () => {
        document.querySelectorAll('[data-unit-height]').forEach(b => b.classList.remove('active'));
        btn.classList.add('active');
        document.getElementById('half-height-unit').value = btn.dataset.unitHeight === 'half' ? '1' : '0';
        doGenerate();
      });
    });

Replace:

        download: 'M1730-Scale.svg',

with:

        download: 'E440-Scale.svg',
  • Step 5: Update the source link and version tag

Replace:

  <span class="version-tag">v{{ version }} — <a href="https://git.baumann.gr/adebaumann/M1730-Scale" target="_blank" rel="noopener">Source</a></span>

with:

  <span class="version-tag">v{{ version }} — <a href="https://git.baumann.gr/adebaumann/E440-Scale" target="_blank" rel="noopener">Source</a></span>
  • Step 6: Run the app locally and verify the form generates a scale
cd /home/adebaumann/development/44-Scale
source .venv/bin/activate 2>/dev/null || true
(cd web && python app.py &) 
sleep 2
curl -s -X POST http://localhost:5000/generate \
  -F unit=uA -F min=0 -F max=50 -F left_label=$'Text\nLeft' -F right_label=$'Text\nRight' \
  -F big_ticks=5 -F small_ticks=9 -F label_count=6 -F output_mode=paths -F scale_type=linear \
  -F label_size=5.1126 -F font=alte-din-1451-mittelschrift.regular \
  | head -c 300
echo
pkill -f "python app.py" || true

Expected: response starts with <?xml version="1.0"... and contains no error text. Then also open http://localhost:5000/ in a browser (or note that a headless check isn't sufficient) to visually confirm the form renders and the light-table preview shows an arc dial, not a strip.

  • Step 7: Commit
git add web/templates/index.html
git commit -m "Update web form for arc-dial fields: unit, left/right labels"

Task 8: Manual visual verification against the reference

Files: none modified — verification only.

Interfaces:

  • Consumes: the fully working generator from Tasks 17.

  • Step 1: Generate the reference-matching scale and save it

cd /home/adebaumann/development/44-Scale
python3 -c "
import sys; sys.path.insert(0, 'web')
from app import generate_svg
svg = generate_svg('uA', 0, 50, 'Text\nLeft', 'Text\nRight', 5, 9, 6,
                   use_paths=False, font_key='alte-din-1451-mittelschrift.regular')
open('/tmp/e440-generated-check.svg', 'w').write(svg)
print('written')
"
  • Step 2: Open both files in a browser or Inkscape and compare side-by-side
xdg-open /home/adebaumann/development/44-Scale/e440.svg 2>/dev/null || true
xdg-open /tmp/e440-generated-check.svg 2>/dev/null || true

Check specifically:

  • Tick marks form a symmetric fan matching the reference's angular spread (not compressed/stretched/mirrored).
  • The rim arc (<path> with stroke #000000) follows the same curve as the ticks' base, with correct orientation (not flipped inside-out — if the arc bulges the wrong way, its sweep-flag in Task 2 Step 4 needs flipping from 0 0 1 to 0 0 0).
  • Numbers 050 sit just outside the tick tips, tilted to match their tick's angle, same reading direction as the reference (not upside-down).
  • uA sits centered above the arc; Text/Left and Text/Right sit in the bottom corners matching the reference's placeholder positions.

If the rim arc orientation is wrong, fix it now in web/app.py (Task 2 Step 4's sweep-flag) and re-run Step 1.

  • Step 3: No commit for this task (verification only; any fixes found get their own commit against the task that introduced the bug).

Task 9: Produce the three deliverable SVG files; remove the old M1730 files

Files:

  • Create: E440-Scale_template.svg (renamed/tidied from e440.svg)
  • Create: E440-Scale_paths.svg
  • Create: E440-Scale_text.svg
  • Delete: e440.svg, M1730-Scale_template.svg, M1730-Scale_paths.svg, M1730-Scale_text.svg

Interfaces:

  • Consumes: generate_svg() from Tasks 16, run with the reference parameters (unit uA, 050, 5 major/9 minor/6 labels, left Text\nLeft, right Text\nRight, font alte-din-1451-mittelschrift.regular).

  • Step 1: Create the template file

cd /home/adebaumann/development/44-Scale
git mv e440.svg E440-Scale_template.svg

Edit E440-Scale_template.svg: change sodipodi:docname="e440.svg" to sodipodi:docname="E440-Scale.svg".

  • Step 2: Generate the paths (outline) variant
cd /home/adebaumann/development/44-Scale
python3 -c "
import sys; sys.path.insert(0, 'web')
from app import generate_svg
svg = generate_svg('uA', 0, 50, 'Text\nLeft', 'Text\nRight', 5, 9, 6,
                   use_paths=True, font_key='alte-din-1451-mittelschrift.regular')
open('E440-Scale_paths.svg', 'w').write(svg + '\n')
"
  • Step 3: Generate the text (embedded-font) variant
cd /home/adebaumann/development/44-Scale
python3 -c "
import sys; sys.path.insert(0, 'web')
from app import generate_svg
svg = generate_svg('uA', 0, 50, 'Text\nLeft', 'Text\nRight', 5, 9, 6,
                   use_paths=False, font_key='alte-din-1451-mittelschrift.regular')
open('E440-Scale_text.svg', 'w').write(svg + '\n')
"
  • Step 4: Delete the old M1730 files
cd /home/adebaumann/development/44-Scale
git rm M1730-Scale_template.svg M1730-Scale_paths.svg M1730-Scale_text.svg
  • Step 5: Verify all three new files are well-formed XML
cd /home/adebaumann/development/44-Scale
python3 -c "
from defusedxml.ElementTree import parse
for f in ('E440-Scale_template.svg', 'E440-Scale_paths.svg', 'E440-Scale_text.svg'):
    parse(f)
    print(f, 'OK')
"

Expected: OK printed for all three files. (Requires defusedxmlpip install defusedxml if it's not already available; stdlib XML parsers are avoided here per repo security guidance since they're vulnerable to XXE/billion-laughs by default, even though these particular files are locally generated and trusted.)

  • Step 6: Commit
git add E440-Scale_template.svg E440-Scale_paths.svg E440-Scale_text.svg
git commit -m "Replace M1730 deliverable SVGs with generated E440 template/paths/text files"

Task 10: Rebrand infra and docs (Dockerfile check, k8s, README.md, CLAUDE.md, VERSION bump)

Files:

  • Modify: k8s/deployment.yaml, k8s/ingress.yaml, k8s/service.yaml
  • Modify: README.md, CLAUDE.md
  • Modify: web/app.py:12 (VERSION)

Interfaces: none — this is the final documentation/infra pass.

  • Step 1: Bump the app version

In web/app.py, change:

VERSION = "0.87"

to:

VERSION = "1.0"
  • Step 2: Rename k8s resources from m1730 to e440

In k8s/deployment.yaml, replace every occurrence of m1730-generator with e440-generator, and the image tag:

          image: git.baumann.gr/adebaumann/m1730-generator:0.87

with:

          image: git.baumann.gr/adebaumann/e440-generator:1.0

In k8s/ingress.yaml, replace every occurrence of m1730-strip-prefix with e440-strip-prefix, m1730-generator with e440-generator, and /M1730-generator with /E440-generator.

In k8s/service.yaml, replace every occurrence of m1730-generator with e440-generator.

  • Step 3: Verify no m1730/M1730 strings remain in k8s manifests
cd /home/adebaumann/development/44-Scale
grep -ri m1730 k8s/ && echo "FOUND STALE REFS" || echo "clean"

Expected: clean.

  • Step 4: Rewrite README.md

Replace the full contents of README.md with:

# E440-Scale

SVG scale artwork and web generator for E440-style round-dial analog panel
meters. Scales are typeset in **Alte DIN 1451 Mittelschrift**.

## Repository layout

E440-Scale_template.svg Inkscape working file (case body, tick marks, labels) E440-Scale_paths.svg Paths-only export — text converted to outlines E440-Scale_text.svg Live-text export — embedded font, editable labels fonts/ TTF fonts (required for Docker build) web/ Flask web generator k8s/ Kubernetes manifests Dockerfile Container build


## SVG files

The canvas is **93.824249 × 57.215687 mm**. The coordinate origin is
offset: layer 1 carries `transform="translate(-60.717741,-107.14126)"`, so
absolute SVG coordinates begin around x=61, y=107.

The dial's case geometry — body outline, mounting ears, pivot point
(local `107.42845, 162.62819`), rim radius (`43.842751` mm), and angular
sweep (`±45.667°` from vertical) — is fixed across every generated scale.
Only the tick marks, numeric labels, unit label, and corner text vary.

| File | Purpose |
|---|---|
| `E440-Scale_template.svg` | Master Inkscape file. Edit this one. |
| `E440-Scale_paths.svg` | All text converted to outlines. Use for cutting plotters, laser engravers, or any workflow where fonts must not be embedded as live text. |
| `E440-Scale_text.svg` | Live `<text>` nodes with the font embedded as base64. Smaller file; requires an SVG viewer that supports embedded fonts. |

Open files in **Inkscape** (developed with v1.4.3). There is no build step
— edit the SVG directly or use the web generator.

## Web generator

A Flask app that generates arc-dial scale SVGs from a form. Supports two
output modes switchable in the UI:

- **Paths** — glyphs rendered as outlines via fontTools; opens cleanly in
  Inkscape, Illustrator, and cutting software
- **Text** — live `<text>` elements with the selected font embedded as
  base64

### Run locally

```bash
source .venv/bin/activate
cd web && python app.py      # http://localhost:5000

Fonts are loaded at startup from the fonts/ directory in the repo root.

Form parameters

Field Description
Unit Symbol shown centered above the arc (e.g. uA, V, %). Supports inline markup — see below
Left label / Right label Up to 2 lines each, placed in the bottom-left/bottom-right corners of the dial
Mapping Linear, (square root), Log, or 1/x (reciprocal). Major ticks are always evenly spaced angularly; the mapping controls their values and how minor ticks are placed within each interval.
Min / Max Numeric range of the scale
Major intervals Number of major tick intervals
Minor per interval Minor ticks between each pair of major ticks. When odd, the exact midpoint subdivision is drawn as a taller "medium" tick, matching the reference dial's 3-tier tick scheme
Labels Number of numeric labels; Labels 1 should divide evenly into Major intervals
Custom labels Optional comma-separated labels that override the calculated numbers
Label size (mm) Font size of the numeric labels, in millimetres (default 5.1126)

Scale mappings

Mapping Major tick values Minor tick placement Constraints
Linear Evenly spaced Uniform None
Quadratic (0, 1, 4, 9 … × max) — compresses low values Crowded toward the low end of each interval Min ≥ 0
Log Geometric (min × r⁰, r¹, r² …) — each interval spans the same ratio Crowded toward the high end of each interval Min and Max > 0
1/x Harmonic (values decrease left → right, e.g. ∞, 10, 5, 2, 1) — compresses high values Uniform (in angle space, i.e. proportional to 1/v) Min and Max > 0; Max may be ∞

Unit markup

The Unit field accepts a lightweight shorthand (no LaTeX engine — everything is drawn as glyph paths and auto-sized to fit above the arc):

Markup Result
cm^2 superscript (^{...} for multiple characters)
H_2O subscript (_{...} for multiple characters)
{1/2} stacked fraction with a horizontal rule; a bare / stays inline

Constructs nest: {m/s^2} is a fraction with a superscript in the denominator, x^{1/2} a superscripted fraction.

Container

docker build -t e440-scale:latest .
docker run -p 5000:5000 e440-scale:latest

Fonts are loaded from fonts/ in the repo root; they are bundled into the image at build time.

The production image is at git.baumann.gr/adebaumann/e440-generator.

Kubernetes

Manifests in k8s/ deploy to the works namespace with a Traefik v2 IngressRoute at works.baumann.gr/E440-generator.

# Managed by ArgoCD — push to git to deploy
git push

Resources: namespace, deployment (1 replica, gunicorn), ClusterIP service, Traefik IngressRoute + stripPrefix middleware.

License

GPL-3.0 — see LICENSE.


- [ ] **Step 5: Rewrite `CLAUDE.md`**

Replace the full contents of `CLAUDE.md` with:

```markdown
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

SVG scale artwork for E440-style round-dial analog meters. The scales are designed in Inkscape (v1.4.3) using **Alte DIN 1451 Mittelschrift**, a German technical drafting standard typeface.

The canvas is **93.824249 mm × 57.215687 mm**. The coordinate origin is offset: `transform="translate(-60.717741,-107.14126)"` is applied to `layer1` in all files, so absolute SVG coordinates start around x=61, y=107 rather than 0,0.

## File Roles

The three SVG files represent layers of the same design, intended to be combined or used separately:

- **`E440-Scale_template.svg`** — Inkscape working template. Contains the case body path (with mounting ears and rim cutout), the pivot guide circle, tick marks, numeric labels `0``50`, the `uA` unit label, and `Text`/`Left` and `Text`/`Right` corner label placeholders.
- **`E440-Scale_paths.svg`** — Paths-only version: all tick marks and text as `<path>`/`<line>` elements, no live text. Used for cutting/printing workflows where fonts must not be embedded as live text.
- **`E440-Scale_text.svg`** — Text-overlay version: tick mark lines plus live `<text>` elements for numeric/unit/corner labels, font embedded as base64.

## Key Design Parameters

- Stroke colors: `#1a1a1a` (dark near-black) for tick marks, numeric/unit/corner text; fill `#f9f9f9` for the case body; `#000000` for the rim arc stroke; `#b3b3b3` for the pivot guide circle.
- Fixed case geometry (identical across every generated scale — only tick/label/text content varies):
  - Pivot point (local, pre-translate frame): `(107.42845, 162.62819)`
  - Rim / inner-tick radius: `43.842751` mm
  - Angular sweep: `±45.667193°` from vertical, clockwise-positive (low value on the left)
  - Tick radial lengths: major `4.7839203` mm, medium `3.1215858` mm, small `1.8121802` mm
  - Tick stroke widths: major `0.79658329`, medium `0.37985462`, small `0.27478597`
  - Label radius: `49.862` mm
- Tick tiers: major ticks at each labeled interval boundary; a medium tick appears only when the minor-tick count per interval is odd, at the exact midpoint subdivision; all other subdivisions render as small ticks.
- Numeric labels rotate to follow the arc (radial orientation), matching their tick's angle.

## Web Generator (`web/`)

A Flask app that generates scale SVGs from a form. The venv lives at the repo root.

source .venv/bin/activate cd web && python app.py # runs on http://localhost:5000


`app.py` has one route (`GET /`) serving the form and one (`POST /generate`) that returns SVG. All SVG geometry is computed in `generate_svg()` using the same coordinate constants as the hand-drawn files: ticks and labels are positioned "as if angle=0" (straight up from the pivot) and given an SVG `rotate(angle, PIVOT_X, PIVOT_Y)` transform. The form exposes: unit label, min/max values, scale mapping, major tick intervals, minor ticks per interval, number of numeric labels, custom labels, label size, font, output mode, and left/right corner labels (up to 2 lines each).

Fonts are loaded at startup from every `.ttf` in `fonts/` and embedded as base64 in every generated SVG (text output mode) so output files are self-contained. The default font is `alte-din-1451-mittelschrift.regular`.

## Container & Kubernetes

docker build -t e440-scale:latest .


`fonts/` must contain the required TTF files before building.

kubectl apply -f k8s/


`k8s/ingress.yaml` adds a Traefik `IngressRoute` for `works.baumann.gr/E440-generator` and a `stripPrefix` middleware. Adjust `certResolver` in the file if your Traefik instance uses a different name.

## Editing SVG files directly

Open files in **Inkscape**. There is no build, lint, or test tooling — changes are made directly to the SVG XML or via Inkscape's GUI. When modifying tick positions or labels, keep all three files consistent.
  • Step 6: Verify the Docker build still works with the renamed image tag
cd /home/adebaumann/development/44-Scale
docker build -t e440-scale:latest . 2>&1 | tail -20

Expected: build completes successfully (Successfully tagged e440-scale:latest or equivalent final layer message). If Docker isn't available in this environment, skip this step and note it in the final report instead of claiming it was verified.

  • Step 7: Commit
git add web/app.py k8s/ README.md CLAUDE.md
git commit -m "Rebrand infra and docs from M1730 to E440; bump version to 1.0"

Final check

  • Run grep -ril m1730 --include='*.py' --include='*.html' --include='*.yaml' --include='*.md' . from the repo root and confirm no output (aside from this plan/spec doc's historical references, which are expected to mention M1730 as the prior state).
  • Confirm git status shows a clean tree with all changes committed.
  • Confirm the three deliverable SVGs (E440-Scale_template.svg, E440-Scale_paths.svg, E440-Scale_text.svg) exist and the three old M1730-Scale_*.svg files are gone.