Files
openvpncertupdate/docs/superpowers/plans/2026-07-08-tui-list-emails.md
Vlad Doloman 6c60a63eb2 Add planning docs
Track the superpowers planning documents under docs/superpowers/plans/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 23:56:29 +03:00

5.9 KiB

TUI Main Menu Email Column 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: Show each cert's email address alongside its CN in the TUI main menu's cert list rows.

Architecture: CertInfo.email is already populated by load_expiring_certs()/load_all_certs() from index.txt (_parse_index_line()), and show_main_screen() already receives the full certs: list[CertInfo]. This is a pure rendering change: extend the existing row-format string (openvpncertupdate.py, inside show_main_screen()'s render loop) to append the email, padding the CN column to a fixed width so columns stay aligned. No new data plumbing.

Tech Stack: Python 3.9 stdlib curses. Tests via pytest + unittest.mock, matching tests/test_main_screen.py's existing conventions (mocked stdscr, inspecting stdscr.addstr.call_args_list).

Global Constraints

  • Single-file script: change stays in openvpncertupdate.py, no new modules.
  • Applies to both the expiring-only and "all certs" (A-toggle) views — they share this same render loop, so no separate handling needed.
  • Unknown email renders as blank (not a placeholder string like (none)) — matches the TUI's existing minimalist row style; this is a deliberate difference from the --list/--list-all CLI table, which does use (none).
  • CN column gets a fixed width of 20 characters (padding with spaces), consistent with the existing fixed 18-char width already used for the expiry label column on the same line.

File Structure

  • Modify openvpncertupdate.py: one line inside show_main_screen()'s render loop (currently line = f" {box} {lbl:<18} {cert.cn}", around line 941 — find it via _expiry_label(cert) immediately above it).
  • Modify tests/test_main_screen.py: extend the _cert() test helper to accept an optional email argument, and add two new tests.

Task 1: Add email column to the cert list row

Files:

  • Modify: openvpncertupdate.py (inside show_main_screen(), the if is_cert(i): branch of the render loop)
  • Modify: tests/test_main_screen.py (_cert() helper + new tests)

Interfaces:

  • Consumes: CertInfo.email: str (already exists, default ""), CertInfo.cn: str.

  • Produces: nothing new — this is a leaf rendering change with no other task depending on it.

  • Step 1: Extend the _cert() test helper

In tests/test_main_screen.py, replace:

def _cert(cn: str, days_left: int) -> CertInfo:
    """Build a minimal CertInfo; expires value is a plausible UTC datetime."""
    return CertInfo(
        cn=cn,
        expires=datetime(2030, 1, 1, tzinfo=timezone.utc),
        days_left=days_left,
    )

with:

def _cert(cn: str, days_left: int, email: str = "") -> CertInfo:
    """Build a minimal CertInfo; expires value is a plausible UTC datetime."""
    return CertInfo(
        cn=cn,
        expires=datetime(2030, 1, 1, tzinfo=timezone.utc),
        days_left=days_left,
        email=email,
    )
  • Step 2: Write the failing tests

In tests/test_main_screen.py, add directly after test_cursor_row_has_reverse_attribute (search for that function name):

def test_cert_row_shows_email():
    certs = [_cert("alice", 10, email="alice@example.com"), _cert("bob", 3)]
    stdscr = _make_stdscr()
    stdscr.getch.side_effect = [ord("q")]
    with patch("openvpncertupdate.init_colors"):
        show_main_screen(stdscr, certs)
    alice_calls = [c for c in stdscr.addstr.call_args_list if "alice" in str(c.args[2])]
    assert alice_calls
    assert "alice@example.com" in alice_calls[0].args[2]


def test_cert_row_blank_when_no_email():
    certs = [_cert("bob", 3)]  # email defaults to ""
    stdscr = _make_stdscr()
    stdscr.getch.side_effect = [ord("q")]
    with patch("openvpncertupdate.init_colors"):
        show_main_screen(stdscr, certs)
    bob_calls = [c for c in stdscr.addstr.call_args_list if "bob" in str(c.args[2])]
    assert bob_calls
    assert "(none)" not in bob_calls[0].args[2]
  • Step 3: Run tests to verify they fail

Run: python3 -m pytest tests/test_main_screen.py -k cert_row_shows_email -v Expected: FAIL — assert "alice@example.com" in alice_calls[0].args[2] fails because the row doesn't include the email yet (test_cert_row_blank_when_no_email will already pass since it only checks for absence of "(none)", which is fine — it's asserting current behavior stays true after the change too, so it's not required to fail here, but run both to confirm the shows_email one fails)

  • Step 4: Update the row format

In openvpncertupdate.py, inside show_main_screen(), find:

            if is_cert(i):
                cert = certs[i]
                box  = "[X]" if cert.cn in checked else "[ ]"
                lbl  = _expiry_label(cert)
                line = f"  {box} {lbl:<18} {cert.cn}"

Replace the line = ... assignment with:

                line = f"  {box} {lbl:<18} {cert.cn:<20} {cert.email}"
  • Step 5: Run tests to verify they pass

Run: python3 -m pytest tests/test_main_screen.py -v Expected: all tests in the file PASS (this also re-verifies no regressions in the pre-existing tests, since the row content assertions there — e.g. test_cursor_row_has_reverse_attribute — check for "bob"/"alice" substrings, not exact row content, so the added columns don't break them)

  • Step 6: Run the full suite

Run: python3 -m pytest tests/ -v Expected: all tests PASS, no regressions in other files

  • Step 7: Commit
git add openvpncertupdate.py tests/test_main_screen.py
git commit -m "show email alongside CN in TUI main menu cert list"

Final Verification

  • Run python3 -m pytest tests/ -v — all tests pass, count should be 141 (prior) + 2 new = 143.