Linux, Python, vim, git & nix LPvgn Short Stack
Future-proof your skills and escape the tech hamster wheel with Linux, Python, vim & git — now with nix (LPvgn), an AI stack to resist obsolescence. Follow along as I build next generation AI/SEO tools for porting Jupyter Notebooks to FastHTML / HTMX Web apps using the Pipulate free AI SEO software.

Co-location Over Abstraction: A Python Refactoring Lesson

I hit a wall with my own code. I’d created an abstraction that was supposed to be clean, but it just made updating the content a frustrating chore. It forced me to jump between two different parts of the file to make a single change. I realized I was fighting my own framework’s philosophy, so I had the AI refactor it to co-locate the data and the presentation logic. The result was simpler, more maintainable, and a fantastic lesson in prioritizing developer experience over rigid rules.

Setting the Stage: Context for the Curious Book Reader

Context for the Curious Book Reader

This journal entry captures a pivotal ‘aha!’ moment in software development: realizing that a widely accepted ‘best practice’—separating data from its presentation—was actually making the code harder to maintain. It documents a hands-on refactoring session, aided by an AI, to simplify a Python application by prioritizing developer experience (DX) and co-locating code that changes together. This is a real-world example of pragmatism trumping dogmatism in the pursuit of more readable and maintainable code.


Technical Journal Entry Begins

After racking my brain for hours and days on complex refactoring issues and bug squashing I decided to throw myself a softball. Now that the Introduction plugin app is the homepage it’s time to continuously refine it to make Pipulate more immediately useful and less intimidating, not only to the user facing the installed app for the first time but for the developer having to control the messaging there. And it’s time to just have the AI do the coding!

The Developer’s Dilemma: When Abstraction Hurts

Me: The developer friendliness on 010_introduction.py is too challenging. When a page is updated, the developer has to keep everything in the complex get_intro_page_data dict in sync with the keys from it that are used down in the create_page_content method. It’s nearly impossible for a human to keep it all in sync editing the dict up there and the use of the keys down there. Everything should just be edited “in location” to feel natural as is the core design principle of HTMX using Python elements as the template language. This extra layer of abstraction is working against me.

BEFORE:

"""
Introduction Plugin - Extracted intro pages from original homepage

This plugin serves the 4-page introduction sequence that was originally
the homepage content before switching to the Roles app.
"""

import logging
from fasthtml.common import *
from pathlib import Path
import urllib.parse

ROLES = ['Core']

logger = logging.getLogger(__name__)

class IntroductionPlugin:
    NAME = "introduction"
    DISPLAY_NAME = "Introduction 💬"
    ENDPOINT_MESSAGE = "💬 Introduction Guide: Learn about Pipulate's layout, features, and how to get started effectively. This comprehensive guide covers profiles, workflows, and the local LLM assistant."

    def __init__(self, app, pipulate, pipeline, db):
        logger.debug(f"IntroductionPlugin initialized with NAME: {self.NAME}")
        self.app = app
        self.pipulate = pipulate
        self.pipeline = pipeline
        pip = self.pipulate
        self._has_sent_endpoint_message = False  # Flag to track if we've sent the initial endpoint message
        self._last_streamed_page = None  # Track the last page we sent to LLM

        # Register routes for page navigation
        app.route('/introduction/page/{page_num}', methods=['GET', 'POST'])(self.serve_page)

    def get_intro_page_data(self, page_num: int, app_name: str, model: str):
        """Returns page data for intro pages."""
        pages = {
            1: {
                'title': f'Welcome to {app_name} 🎯',
                'features': [
                    ('👤 PROFILE', 'Set up Client (aka Customer) profiles. Each is their own separate workspace. In other words, they each get their own separate Task List.'),
                    ('⚡ APP', 'For each Profile (Client/Customer), try each APP (Parameter Buster for example).'),
                    ('DEV/Prod', 'Use DEV mode for practice. Use Prod mode in front of your Client or Customer.')
                ],
                'secret_word': 'FOUNDATION'
            },
            2: {
                'title': 'Local AI Assistant Setup 🤖',
                'intro_text': f'{app_name} uses a separately installed local chatbot called Ollama. Having a local LLM installed is not required for {app_name} to keep working, but is encouraged because it will keep an AI "in the loop" to provide context-aware advice.',
                'benefits': [
                    ('🔒 PRIVATE', 'No registration or API token required. Completely local and private.'),
                    ('💰 FREE', 'Free for the price of electricity - no monthly subscriptions.'),
                    ('🧠 CONTEXT-AWARE', 'Always knows what you\'re doing and can provide specific advice.'),
                    ('🚀 INSTANT', 'No network delays - responds immediately from your machine.')
                ],
                'installation_title': 'Installation Steps',
                'installation_steps': [
                    ('DOWNLOAD OLLAMA', 'Ollama has custom Mac and Windows installers - use them for best results.'),
                    ('LOAD GEMMA 3', 'Once Ollama is installed, open a Terminal and type "ollama pull gemma3".'),
                    ('EXPERIMENT', 'Feel free to try other models once you\'re comfortable with the basics.')
                ],
                'fallback_note': f'If you don\'t take this step, the majority of {app_name} will still work — just without an AI riding shotgun.',
                'secret_word': 'OLLAMA'
            },
            3: {
                'experimenting_title': 'Positive First Experience',
                'experimenting_steps': [
                    ('🚀 START', 'in DEV mode. Practice! Try stuff like resetting the entire database 🔄 (in 🤖). Experiment and get comfortable. You can do no harm. This is what DEV mode is for.'),
                    ('👥 PROFILES', 'Add them. Rearrange them. Check and uncheck them. Changes are reflected instantly in the PROFILE menu. Notice how "Lock" works to help avoid accidentally showing other Client (Nick)names to each other.'),
                    ('⚡ WORKFLOWS', f'Try the Hello Workflow to get a feel for how {app_name} workflows work.')
                ],
                'interface_title': 'Understanding the Interface',
                'interface_items': [
                    ('👤 PROFILES', 'Give Clients cute nicknames in Prod mode (Appliances, Sneakers, etc). Resetting database won\'t delete. So experiment in DEV and let permanent choices "settle in" in Prod.'),
                    ('📊 APPS', "Botify folks: try Parameter Buster on your Client. It's a big potential win."),
                    ('🔗 LINK GRAPH', "Botify folks: try Link Graph Visualizer to explore internal linking patterns.")
                ],
                'secret_word': 'PRACTICE'
            },
            4: {
                'title': 'Tips for Effective Use',
                'tips': [
                    ('🔗 CONNECT', 'Set up your API keys to activate Botify-integrated workflows such as Parameter Buster and Link Graph Visualizer.'),
                    ('🗑️ DELETE', 'Workflows are disposable because they are so easily re-created. So if you lose a particular workflow, just make it again with the same inputs 🤯'),
                    ('💾 SAVE', 'Anything you do that has side-effects like CSVs stays on your computer even when you delete the workflows. Browse directly to files or attach new workflows to them by using the same input. Caveat: a complete reinstall using that "rm -rf ~/Botifython" command will delete everything.'),
                    ('🔒 LOCK', 'Lock PROFILE to avoid showing other Client (Nick)names to each other.'),
                    ('📁 BROWSE', 'Go look where things are saved.')
                ],
                'secret_word': 'WORKFLOW'
            },
            5: {
                'title': 'The Localhost Advantage 🏠',
                'intro_text': f'You\'ve paid the price of a more difficult install than a cloud app. Congratulations! Time to reap the rewards.',
                'advantages': [
                    ('🔐 BROWSER LOGINS', 'Access web UIs like Botify, SEMRush, ahrefs without APIs. Browser saves passwords locally, nothing leaves your machine.'),
                    ('💾 PERSISTENT FILES', 'All CSVs and web scrapes stay on your machine for browsing. No daily clearing like Google Colab.'),
                    ('⏱️ LONG-RUNNING WORKFLOWS', 'Crawls can run 24+ hours without being "shut down" for resource usage. No cloud time limits.'),
                    ('🛡️ VPN FLEXIBILITY', 'Use your VPN to control web traffic appearance. No known cloud IPs or complex IP-hiding costs.')
                ],
                'benefits_title': 'Real-World Benefits',
                'benefits': [
                    ('Control your data', 'Everything stays local and under your control.'),
                    ('No artificial limits', 'Run workflows as long as needed.'),
                    ('Use existing tools', 'Leverage your VPN and browser setup.'),
                    ('Browse files naturally', 'Access outputs like any local file.')
                ],
                'secret_word': 'LOCALHOST'
            },
            6: {
                'title': 'Local LLM Assistant 🤖',
                'llm_features': [
                    ('🔒 PRIVACY', 'All conversations stay on your machine. No data is sent to external servers.'),
                    ('🧠 CONTEXT', 'The LLM understands your current workflow and can help with specific tasks.'),
                    ('💡 GUIDANCE', 'Ask questions about workflows, get help with API keys, or request explanations.'),
                    ('🔗 INTEGRATION', 'The LLM is aware of your current profile, environment, and active workflow.'),
                    ('⚡ REAL-TIME', 'Chat updates in real-time as you progress through workflows.')
                ],
                'usage_tips': [
                    'Try asking "What can I do with this workflow?" when starting a new one.',
                    'Ask for help with specific steps if you get stuck.',
                    'Request explanations of workflow outputs or data.',
                    'Get suggestions for next steps or alternative approaches.'
                ],
                'secret_word': 'ASSISTANT'
            },
            7: {
                'title': 'Background LLM Training',
                'intro_text': f'🧠 {app_name} automatically trains your local LLM as you navigate. The LLM learns what you\'re viewing without cluttering your chat.',
                'how_it_works': [
                    ('SILENT UPDATES', 'Page content added to LLM history in the background.'),
                    ('SECRET WORDS', 'Each page has a secret word proving successful training.'),
                    ('CONTEXTUAL AWARENESS', 'LLM can answer questions about content you\'ve viewed.')
                ],
                'examples_title': 'Where This Works',
                'examples': [
                    ('📖 Introduction Pages', 'Each page trains the LLM with its content and secret word.'),
                    ('🐇 Workflows', 'Starting workflows loads specific training content.'),
                    ('📚 Documentation', 'Viewing docs automatically adds full content to LLM context.')
                ],
                'testing_tip': 'Ask: "What are all the secret words from the Introduction pages?"',
                'secret_word': 'CONTEXT'
            }
        }
        return pages.get(page_num)

    def create_page_content(self, page_num: int, app_name: str, model: str):
        """Create FastHTML content for a specific page."""
        page_data = self.get_intro_page_data(page_num, app_name, model)

        if not page_data:
            return Card(
                H3("Page Not Found"),
                P(f"Introduction page {page_num} not found."),
                cls="min-height-300"
            )

        card_class = "intro-card"

        if page_num == 1:
            return Card(
                H2(page_data['title']),
                Ol(*[Li(Strong(f'{name}:'), f' {desc}') for name, desc in page_data['features']]),
                P(f'The chat interface on the right is optionally powered by a local LLM ({model}) to assist you 🤖. Click ',
                  A('Next ▸', 
                    hx_post='/introduction/page/2',
                    hx_target='#grid-left-content',
                    hx_swap='innerHTML',
                    cls='link-inherit-underline',
                    onmouseover='this.style.color="#007bff";',
                    onmouseout='this.style.color="inherit";'),
                  ' to continue.'),
                cls=card_class
            )

        elif page_num == 2:
            return Card(
                H3(page_data['title']),
                P(page_data['intro_text']),
                Ol(*[Li(Strong(f'{name}:'), f' {desc}') for name, desc in page_data['benefits']]),
                H3(page_data['installation_title']),
                Ol(
                    Li(
                        Strong(
                            A('DOWNLOAD OLLAMA', 
                              href='https://ollama.com/', 
                              target='_blank',
                              cls='link-inherit-plain',
                              onmouseover='this.style.textDecoration="underline"; this.style.color="#007bff";',
                              onmouseout='this.style.textDecoration="none"; this.style.color="inherit";'),
                            Img(src='/assets/feather/external-link.svg', 
                                alt='External link', 
                                style='width: 14px; height: 14px; margin-left: 0.25rem; vertical-align: middle; filter: brightness(0) invert(1);'),
                            ':'
                        ), 
                        ' Ollama has custom Mac and Windows installers - use them for best results.'
                    ),
                    *[Li(Strong(f'{name}:'), f' {desc}') for name, desc in page_data['installation_steps'][1:]]
                ),
                P(page_data['fallback_note']),
                cls=card_class
            )

        elif page_num == 3:
            return Card(
                H3(page_data['experimenting_title']),
                Ol(*[Li(Strong(f'{name}:'), f' {desc}') for name, desc in page_data['experimenting_steps']]),
                H3(page_data['interface_title']),
                Ul(*[Li(Strong(f'{name}:'), f' {desc}') for name, desc in page_data['interface_items']]),
                cls=card_class
            )

        elif page_num == 4:
            return Card(
                H3(page_data['title']),
                Ol(*[Li(Strong(f'{name}:'), f' {desc}') for name, desc in page_data['tips']]),
                Hr(),
                P('Try it now: ', A('Open Downloads Folder',
                                   href='/open-folder?path=' + urllib.parse.quote(str(Path('downloads').absolute())),
                                   hx_get='/open-folder?path=' + urllib.parse.quote(str(Path('downloads').absolute())),
                                   hx_swap='none')),
                cls=card_class
            )

        elif page_num == 5:
            return Card(
                H3(page_data['title']),
                P(page_data['intro_text']),
                Ol(*[Li(Strong(f'{name}:'), f' {desc}') for name, desc in page_data['advantages']]),
                H4(page_data['benefits_title']),
                Ol(*[Li(Strong(f'{name}:'), f' {desc}') for name, desc in page_data['benefits']]),
                cls=card_class
            )

        elif page_num == 6:
            return Card(
                H3(page_data['title']),
                P(f'Your local LLM ({model}) provides intelligent assistance throughout your workflow:'),
                Ol(*[Li(Strong(f'{name}:'), f' {desc}') for name, desc in page_data['llm_features']]),
                H4('How to Use the LLM'),
                Ul(*[Li(tip) for tip in page_data['usage_tips']]),
                cls=card_class
            )

        elif page_num == 7:
            return Card(
                H3(page_data['title']),
                P(page_data['intro_text']),
                H4('How It Works'),
                Ol(*[Li(Strong(f'{name}:'), f' {desc}') for name, desc in page_data['how_it_works']]),
                H4(page_data['examples_title']),
                Ul(*[Li(Strong(f'{name}:'), f' {desc}') for name, desc in page_data['examples']]),
                Hr(),
                P(Strong('🧪 Test the System: '), page_data['testing_tip']),
                cls=card_class
            )


    async def serve_page(self, request):
        """Handle page navigation within the main app framework."""
        page_num = int(request.path_params.get('page_num', '1'))

        # Get app name and model from server settings
        from server import APP_NAME, MODEL

        # Store current page in database
        try:
            self.pipulate.db['intro_current_page'] = str(page_num)
        except Exception as e:
            logger.warning(f"Could not store intro_current_page: {e}")
            # Continue without storing - this is non-critical

        # Return the updated content directly (same as landing method)
        return await self.landing()

    async def landing(self, render_items=None):
        """Always appears in create_grid_left."""
        # Get app name and model from server settings
        from server import APP_NAME, MODEL

        # Get current page from database, default to 1
        current_page = int(self.pipulate.db.get('intro_current_page', '1'))

        # Send the intro message to conversation history, but only once per session
        if self.pipulate is not None:
            try:
                # Add the endpoint message only once
                if not self._has_sent_endpoint_message:
                    self.pipulate.append_to_history(self.ENDPOINT_MESSAGE, role="system")
                    self._has_sent_endpoint_message = True
                    logger.debug("Introduction endpoint message added to conversation history")

            except Exception as e:
                logger.error(f"Error in introduction plugin: {str(e)}")

        # Create navigation arrows (matching original server.py style)
        prev_button = Button(
            '◂ Previous',
            hx_post=f'/introduction/page/{current_page - 1}' if current_page > 1 else '#',
            hx_target='#grid-left-content',
            hx_swap='innerHTML',
            cls=f"{'primary outline' if current_page == 1 else 'primary'} width-160",
            disabled=current_page == 1
        )

        next_button = Button(
            'Next ▸',
            hx_post=f'/introduction/page/{current_page + 1}' if current_page < 7 else '#',
            hx_target='#grid-left-content',
            hx_swap='innerHTML',
            cls=f"{'primary outline' if current_page == 7 else 'primary'} width-160",
            disabled=current_page == 7
        )

        nav_arrows = Div(
            prev_button,
            next_button,
            cls='flex-center-gap'
        )

        # Create the current page content
        page_content = self.create_page_content(current_page, APP_NAME, MODEL)

        return Div(
            H2(f"📖 Introduction Guide - Page {current_page} of 7"),
            nav_arrows,
            page_content,
            nav_arrows
        )

Gemini CLI:

✦ I understand. The abstraction in apps/010_introduction.py is making development difficult by 
  separating the data from its usage. I will refactor it to make it more direct and easier to 
  maintain.

  First, I'll read the file to understand its current structure.
 ╭───────────────────────────────────────╮
 │ ✓  ReadFile apps/010_introduction.py  │
 ╰───────────────────────────────────────╯
✦ You're right, that abstraction is unnecessary and makes the code harder to work with. I'll 
  refactor apps/010_introduction.py to inline the data within the create_page_content function, 
  removing the get_intro_page_data function entirely. This will place the content directly where 
  it's used, making it much easier to edit.
 ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮
 │ ✓  Edit apps/010_introduction.py:     def get_intro_page_data(se... =>     def create_page_con… │
 │                                                                                                 │
 │    ... first 386 lines hidden ...                                                               │
 │    167               cls=card_class                                                             │
 │    168           )                                                                              │
 │    169                                                                                          │
 │    170       elif page_num == 6:                                                                │
 │    171 +         title = 'Local LLM Assistant 🤖'                                               │
 │    172 +         llm_features = [                                                               │
 │    173 +             ('🔒 PRIVACY', 'All conversations stay on your machine. No data            │
 │        is sent to external servers.'),                                                          │
 │    174 +             ('🧠 CONTEXT', 'The LLM understands your current workflow and              │
 │        can help with specific tasks.'),                                                         │
 │    175 +             ('💡 GUIDANCE', 'Ask questions about workflows, get help with              │
 │        API keys, or request explanations.'),                                                    │
 │    176 +             ('🔗 INTEGRATION', 'The LLM is aware of your current profile,              │
 │        environment, and active workflow.'),                                                     │
 │    177 +             ('⚡ REAL-TIME', 'Chat updates in real-time as you progress                │
 │        through workflows.')                                                                     │
 │    178 +         ]                                                                              │
 │    179 +         usage_tips = [                                                                 │
 │    180 +             'Try asking "What can I do with this workflow?" when starting a            │
 │        new one.',                                                                               │
 │    181 +             'Ask for help with specific steps if you get stuck.',                      │
 │    182 +             'Request explanations of workflow outputs or data.',                       │
 │    183 +             'Get suggestions for next steps or alternative approaches.'                │
 │    184 +         ]                                                                              │
 │    185           return Card(                                                                   │
 │    ═════════════════════════════════════════════════════════════════════════════════            │
 │    235 -             H3(page_data['title']),                                                    │
 │    186 +             H3(title),                                                                 │
 │    187               P(f'Your local LLM ({model}) provides intelligent assistance               │
 │          throughout your workflow:'),                                                           │
 │    ═════════════════════════════════════════════════════════════════════════════════            │
 │    237 -             Ol(*[Li(Strong(f'{name}:'), f' {desc}') for name, desc in                  │
 │        page_data['llm_features']]),                                                             │
 │    188 +             Ol(*[Li(Strong(f'{name}:'), f' {desc}') for name, desc in                  │
 │        llm_features]),                                                                          │
 │    189               H4('How to Use the LLM'),                                                  │
 │    ═════════════════════════════════════════════════════════════════════════════════            │
 │    239 -             Ul(*[Li(tip) for tip in page_data['usage_tips']]),                         │
 │    190 +             Ul(*[Li(tip) for tip in usage_tips]),                                      │
 │    191               cls=card_class                                                             │
 │    192           )                                                                              │
 │    193                                                                                          │
 │    194       elif page_num == 7:                                                                │
 │    195 +         title = 'Background LLM Training'                                              │
 │    196 +         intro_text = f'🧠 {app_name} automatically trains your local LLM as            │
 │        you navigate. The LLM learns what you\'re viewing without cluttering your                │
 │        chat.'                                                                                   │
 │    197 +         how_it_works = [                                                               │
 │    198 +             ('SILENT UPDATES', 'Page content added to LLM history in the               │
 │        background.'),                                                                           │
 │    199 +             ('SECRET WORDS', 'Each page has a secret word proving                      │
 │        successful training.'),                                                                  │
 │    200 +             ('CONTEXTUAL AWARENESS', 'LLM can answer questions about                   │
 │        content you\'ve viewed.')                                                                │
 │    201 +         ]                                                                              │
 │    202 +         examples_title = 'Where This Works'                                            │
 │    203 +         examples = [                                                                   │
 │    204 +             ('📖 Introduction Pages', 'Each page trains the LLM with its               │
 │        content and secret word.'),                                                              │
 │    205 +             ('🐇 Workflows', 'Starting workflows loads specific training               │
 │        content.'),                                                                              │
 │    206 +             ('📚 Documentation', 'Viewing docs automatically adds full                 │
 │        content to LLM context.')                                                                │
 │    207 +         ]                                                                              │
 │    208 +         testing_tip = 'Ask: "What are all the secret words from the                    │
 │        Introduction pages?"'                                                                    │
 │    209           return Card(                                                                   │
 │    ═════════════════════════════════════════════════════════════════════════════════            │
 │    245 -             H3(page_data['title']),                                                    │
 │    246 -             P(page_data['intro_text']),                                                │
 │    210 +             H3(title),                                                                 │
 │    211 +             P(intro_text),                                                             │
 │    212               H4('How It Works'),                                                        │
 │    ═════════════════════════════════════════════════════════════════════════════════            │
 │    248 -             Ol(*[Li(Strong(f'{name}:'), f' {desc}') for name, desc in                  │
 │        page_data['how_it_works']]),                                                             │
 │    249 -             H4(page_data['examples_title']),                                           │
 │    250 -             Ul(*[Li(Strong(f'{name}:'), f' {desc}') for name, desc in                  │
 │        page_data['examples']]),                                                                 │
 │    213 +             Ol(*[Li(Strong(f'{name}:'), f' {desc}') for name, desc in                  │
 │        how_it_works]),                                                                          │
 │    214 +             H4(examples_title),                                                        │
 │    215 +             Ul(*[Li(Strong(f'{name}:'), f' {desc}') for name, desc in                  │
 │        examples]),                                                                              │
 │    216               Hr(),                                                                      │
 │    ═════════════════════════════════════════════════════════════════════════════════            │
 │    252 -             P(Strong('🧪 Test the System: '), page_data['testing_tip']),               │
 │    217 +             P(Strong('🧪 Test the System: '), testing_tip),                            │
 │    218               cls=card_class                                                             │
 │    219           )                                                                              │
 │    220                                                                                          │
 │    221 +     else:                                                                              │
 │    222 +         return Card(                                                                   │
 │    223 +             H3("Page Not Found"),                                                      │
 │    224 +             P(f"Introduction page {page_num} not found."),                             │
 │    225 +             cls="min-height-300"                                                       │
 │    226 +         )                                                                              │
 │    227                                                                                          │
 │    228 +                                                                                        │
 │    229   async def serve_page(self, request):                                                   │
 │    230       """Handle page navigation within the main app framework."""                        │
 │    231       page_num = int(request.path_params.get('page_num', '1'))                           │
 ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯
✦ I've refactored the code, and the secret_word variable is no longer used. I will remove it to 
  clean up the code.
 ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮
 │ x  Edit {"file_path":"/home/mike/repos/pipulate/apps/010_introduction.py","instruction":"The '… │
 │                                                                                                 │
 │    A secondary check determined that no changes were necessary to fulfill the                   │
 │    instruction. Explanation: The original search failed because the line                        │
 │    `secret_word = 'FOUNDATION'` was already removed from the file. The file content             │
 │    already matches the intended `replace` string, meaning the change has already                │
 │    been applied. Therefore, no changes are required.. Original error with the                   │
 │    parameters given: Failed to edit, 0 occurrences found for old_string (        if             │
 │    page_num == 1:                                                                               │
 │                title = f'Welcome to {app_name} 🎯'                                              │
 │                features = [                                                                     │
 │                    ('👤 PROFILE', 'Set up Client (aka Customer) profiles. Each is               │
 │    their own separate workspace. In other words, they each get their own separate               │
 │    Task List.'),                                                                                │
 │                    ('⚡ APP', 'For each Profile (Client/Customer), try each APP                 │
 │    (Parameter Buster for example).'),                                                           │
 │                    ('DEV/Prod', 'Use DEV mode for practice. Use Prod mode in front              │
 │    of your Client or Customer.')                                                                │
 │                ]                                                                                │
 │                secret_word = 'FOUNDATION'                                                       │
 │                return Card(                                                                     │
 │                    H2(title),                                                                   │
 │                    Ol(*[Li(Strong(f'{name}:'), f' {desc}') for name, desc in                    │
 │    features]),                                                                                  │
 │                    P(f'The chat interface on the right is optionally powered by a               │
 │    local LLM ({model}) to assist you 🤖. Click ',                                               │
 │                      A('Next ▸',                                                                │
 │                        hx_post='/introduction/page/2',                                          │
 │                        hx_target='#grid-left-content',                                          │
 │                        hx_swap='innerHTML',                                                     │
 │                        cls='link-inherit-underline',                                            │
 │                        onmouseover='this.style.color="#007bff";',                               │
 │                        onmouseout='this.style.color="inherit";'),                               │
 │                      ' to continue.'),                                                          │
 │                    cls=card_class                                                               │
 │                )). Original old_string was (        if page_num == 1:                           │
 │                title = f'Welcome to {app_name} 🎯'                                              │
 │                features = [                                                                     │
 │                    ('👤 PROFILE', 'Set up Client (aka Customer) profiles. Each is               │
 │    their own separate workspace. In other words, they each get their own separate               │
 │    Task List.'),                                                                                │
 │                    ('⚡ APP', 'For each Profile (Client/Customer), try each APP                 │
 │    (Parameter Buster for example).'),                                                           │
 │                    ('DEV/Prod', 'Use DEV mode for practice. Use Prod mode in front              │
 │    of your Client or Customer.')                                                                │
 │                ]                                                                                │
 │                secret_word = 'FOUNDATION'                                                       │
 │                return Card(                                                                     │
 │                    H2(title),                                                                   │
 │                    Ol(*[Li(Strong(f'{name}:'), f' {desc}') for name, desc in                    │
 │    features]),                                                                                  │
 │                    P(f'The chat interface on the right is optionally powered by a               │
 │    local LLM ({model}) to assist you 🤖. Click ',                                               │
 │                      A('Next ▸',                                                                │
 │                        hx_post='/introduction/page/2',                                          │
 │                        hx_target='#grid-left-content',                                          │
 │                        hx_swap='innerHTML',                                                     │
 │                        cls='link-inherit-underline',                                            │
 │                        onmouseover='this.style.color="#007bff";',                               │
 │                        onmouseout='this.style.color="inherit";'),                               │
 │                      ' to continue.'),                                                          │
 │                    cls=card_class                                                               │
 │                )) in /home/mike/repos/pipulate/apps/010_introduction.py. No edits               │
 │    made. The exact text in old_string was not found. Ensure you're not escaping                 │
 │    content incorrectly and check whitespace, indentation, and context. Use read_file            │
 │    tool to verify.                                                                              │
 ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯
✦ It seems the secret_word variable was already removed in the previous step. I'll read the file 
  again to confirm and ensure everything is as expected.
 ╭───────────────────────────────────────╮
 │ ✓  ReadFile apps/010_introduction.py  │
 ╰───────────────────────────────────────╯
✦ The refactoring is complete. I have updated apps/010_introduction.py to make it more 
  developer-friendly by co-locating the page content with its presentation logic. The 
  get_intro_page_data function has been removed, and the content is now directly within the 
  create_page_content function, which should make it much easier to edit and maintain.

                                                        accepting edits (shift + tab to toggle)
╭─────────────────────────────────────────────────────────────────────────────────────────────╮
│ >   Type your message or @path/to/file                                                      │
╰─────────────────────────────────────────────────────────────────────────────────────────────╯
~/repos/pipulate    no sandbox (see     gemini-2.5-pro (98%         | ✖ 4 errors (ctrl+o for 
(main*)             /docs)             context left)                  details)

Me:

AFTER:

"""
Introduction Plugin - Extracted intro pages from original homepage

This plugin serves the 4-page introduction sequence that was originally
the homepage content before switching to the Roles app.
"""

import logging
from fasthtml.common import *
from pathlib import Path
import urllib.parse

ROLES = ['Core']

logger = logging.getLogger(__name__)

class IntroductionPlugin:
    NAME = "introduction"
    DISPLAY_NAME = "Introduction 💬"
    ENDPOINT_MESSAGE = "💬 Introduction Guide: Learn about Pipulate's layout, features, and how to get started effectively. This comprehensive guide covers profiles, workflows, and the local LLM assistant."

    def __init__(self, app, pipulate, pipeline, db):
        logger.debug(f"IntroductionPlugin initialized with NAME: {self.NAME}")
        self.app = app
        self.pipulate = pipulate
        self.pipeline = pipeline
        pip = self.pipulate
        self._has_sent_endpoint_message = False  # Flag to track if we've sent the initial endpoint message
        self._last_streamed_page = None  # Track the last page we sent to LLM

        # Register routes for page navigation
        app.route('/introduction/page/{page_num}', methods=['GET', 'POST'])(self.serve_page)

    def create_page_content(self, page_num: int, app_name: str, model: str):
        """Create FastHTML content for a specific page."""

        card_class = "intro-card"

        if page_num == 1:
            title = f'Welcome to {app_name} 🎯'
            features = [
                ('👤 PROFILE', 'Set up Client (aka Customer) profiles. Each is their own separate workspace. In other words, they each get their own separate Task List.'),
                ('⚡ APP', 'For each Profile (Client/Customer), try each APP (Parameter Buster for example).'),
                ('DEV/Prod', 'Use DEV mode for practice. Use Prod mode in front of your Client or Customer.')
            ]
            return Card(
                H2(title),
                Ol(*[Li(Strong(f'{name}:'), f' {desc}') for name, desc in features]),
                P(f'The chat interface on the right is optionally powered by a local LLM ({model}) to assist you 🤖. Click ',
                  A('Next ▸',
                    hx_post='/introduction/page/2',
                    hx_target='#grid-left-content',
                    hx_swap='innerHTML',
                    cls='link-inherit-underline',
                    onmouseover='this.style.color="#007bff";',
                    onmouseout='this.style.color="inherit";'),
                  ' to continue.'),
                cls=card_class
            )

        elif page_num == 2:
            title = 'Local AI Assistant Setup 🤖'
            intro_text = f'{app_name} uses a separately installed local chatbot called Ollama. Having a local LLM installed is not required for {app_name} to keep working, but is encouraged because it will keep an AI "in the loop" to provide context-aware advice.'
            benefits = [
                ('🔒 PRIVATE', 'No registration or API token required. Completely local and private.'),
                ('💰 FREE', 'Free for the price of electricity - no monthly subscriptions.'),
                ('🧠 CONTEXT-AWARE', 'Always knows what you\'re doing and can provide specific advice.'),
                ('🚀 INSTANT', 'No network delays - responds immediately from your machine.')
            ]
            installation_title = 'Installation Steps'
            installation_steps = [
                ('DOWNLOAD OLLAMA', 'Ollama has custom Mac and Windows installers - use them for best results.'),
                ('LOAD GEMMA 3', 'Once Ollama is installed, open a Terminal and type "ollama pull gemma3".'),
                ('EXPERIMENT', 'Feel free to try other models once you\'re comfortable with the basics.')
            ]
            fallback_note = f'If you don\'t take this step, the majority of {app_name} will still work — just without an AI riding shotgun.'
            return Card(
                H3(title),
                P(intro_text),
                Ol(*[Li(Strong(f'{name}:'), f' {desc}') for name, desc in benefits]),
                H3(installation_title),
                Ol(
                    Li(
                        Strong(
                            A('DOWNLOAD OLLAMA',
                              href='https://ollama.com/',
                              target='_blank',
                              cls='link-inherit-plain',
                              onmouseover='this.style.textDecoration="underline"; this.style.color="#007bff";',
                              onmouseout='this.style.textDecoration="none"; this.style.color="inherit";'),
                            Img(src='/assets/feather/external-link.svg',
                                alt='External link',
                                style='width: 14px; height: 14px; margin-left: 0.25rem; vertical-align: middle; filter: brightness(0) invert(1);'),
                            ':'
                        ),
                        ' Ollama has custom Mac and Windows installers - use them for best results.'
                    ),
                    *[Li(Strong(f'{name}:'), f' {desc}') for name, desc in installation_steps[1:]]
                ),
                P(fallback_note),
                cls=card_class
            )

        elif page_num == 3:
            experimenting_title = 'Positive First Experience'
            experimenting_steps = [
                ('🚀 START', 'in DEV mode. Practice! Try stuff like resetting the entire database 🔄 (in 🤖). Experiment and get comfortable. You can do no harm. This is what DEV mode is for.'),
                ('👥 PROFILES', 'Add them. Rearrange them. Check and uncheck them. Changes are reflected instantly in the PROFILE menu. Notice how "Lock" works to help avoid accidentally showing other Client (Nick)names to each other.'),
                ('⚡ WORKFLOWS', f'Try the Hello Workflow to get a feel for how {app_name} workflows work.')
            ]
            interface_title = 'Understanding the Interface'
            interface_items = [
                ('👤 PROFILES', 'Give Clients cute nicknames in Prod mode (Appliances, Sneakers, etc). Resetting database won\'t delete. So experiment in DEV and let permanent choices "settle in" in Prod.'),
                ('📊 APPS', "Botify folks: try Parameter Buster on your Client. It's a big potential win."),
                ('🔗 LINK GRAPH', "Botify folks: try Link Graph Visualizer to explore internal linking patterns.")
            ]
            return Card(
                H3(experimenting_title),
                Ol(*[Li(Strong(f'{name}:'), f' {desc}') for name, desc in experimenting_steps]),
                H3(interface_title),
                Ul(*[Li(Strong(f'{name}:'), f' {desc}') for name, desc in interface_items]),
                cls=card_class
            )

        elif page_num == 4:
            title = 'Tips for Effective Use'
            tips = [
                ('🔗 CONNECT', 'Set up your API keys to activate Botify-integrated workflows such as Parameter Buster and Link Graph Visualizer.'),
                ('🗑️ DELETE', 'Workflows are disposable because they are so easily re-created. So if you lose a particular workflow, just make it again with the same inputs 🤯'),
                ('💾 SAVE', 'Anything you do that has side-effects like CSVs stays on your computer even when you delete the workflows. Browse directly to files or attach new workflows to them by using the same input. Caveat: a complete reinstall using that "rm -rf ~/Botifython" command will delete everything.'),
                ('🔒 LOCK', 'Lock PROFILE to avoid showing other Client (Nick)names to each other.'),
                ('📁 BROWSE', 'Go look where things are saved.')
            ]
            return Card(
                H3(title),
                Ol(*[Li(Strong(f'{name}:'), f' {desc}') for name, desc in tips]),
                Hr(),
                P('Try it now: ', A('Open Downloads Folder',
                                   href='/open-folder?path=' + urllib.parse.quote(str(Path('downloads').absolute())),
                                   hx_get='/open-folder?path=' + urllib.parse.quote(str(Path('downloads').absolute())),
                                   hx_swap='none')),
                cls=card_class
            )

        elif page_num == 5:
            title = 'The Localhost Advantage 🏠'
            intro_text = f'You\'ve paid the price of a more difficult install than a cloud app. Congratulations! Time to reap the rewards.'
            advantages = [
                ('🔐 BROWSER LOGINS', 'Access web UIs like Botify, SEMRush, ahrefs without APIs. Browser saves passwords locally, nothing leaves your machine.'),
                ('💾 PERSISTENT FILES', 'All CSVs and web scrapes stay on your machine for browsing. No daily clearing like Google Colab.'),
                ('⏱️ LONG-RUNNING WORKFLOWS', 'Crawls can run 24+ hours without being "shut down" for resource usage. No cloud time limits.'),
                ('🛡️ VPN FLEXIBILITY', 'Use your VPN to control web traffic appearance. No known cloud IPs or complex IP-hiding costs.')
            ]
            benefits_title = 'Real-World Benefits'
            benefits = [
                ('Control your data', 'Everything stays local and under your control.'),
                ('No artificial limits', 'Run workflows as long as needed.'),
                ('Use existing tools', 'Leverage your VPN and browser setup.'),
                ('Browse files naturally', 'Access outputs like any local file.')
            ]
            return Card(
                H3(title),
                P(intro_text),
                Ol(*[Li(Strong(f'{name}:'), f' {desc}') for name, desc in advantages]),
                H4(benefits_title),
                Ol(*[Li(Strong(f'{name}:'), f' {desc}') for name, desc in benefits]),
                cls=card_class
            )

        elif page_num == 6:
            title = 'Local LLM Assistant 🤖'
            llm_features = [
                ('🔒 PRIVACY', 'All conversations stay on your machine. No data is sent to external servers.'),
                ('🧠 CONTEXT', 'The LLM understands your current workflow and can help with specific tasks.'),
                ('💡 GUIDANCE', 'Ask questions about workflows, get help with API keys, or request explanations.'),
                ('🔗 INTEGRATION', 'The LLM is aware of your current profile, environment, and active workflow.'),
                ('⚡ REAL-TIME', 'Chat updates in real-time as you progress through workflows.')
            ]
            usage_tips = [
                'Try asking "What can I do with this workflow?" when starting a new one.',
                'Ask for help with specific steps if you get stuck.',
                'Request explanations of workflow outputs or data.',
                'Get suggestions for next steps or alternative approaches.'
            ]
            return Card(
                H3(title),
                P(f'Your local LLM ({model}) provides intelligent assistance throughout your workflow:'),
                Ol(*[Li(Strong(f'{name}:'), f' {desc}') for name, desc in llm_features]),
                H4('How to Use the LLM'),
                Ul(*[Li(tip) for tip in usage_tips]),
                cls=card_class
            )

        elif page_num == 7:
            title = 'Background LLM Training'
            intro_text = f'🧠 {app_name} automatically trains your local LLM as you navigate. The LLM learns what you\'re viewing without cluttering your chat.'
            how_it_works = [
                ('SILENT UPDATES', 'Page content added to LLM history in the background.'),
                ('SECRET WORDS', 'Each page has a secret word proving successful training.'),
                ('CONTEXTUAL AWARENESS', 'LLM can answer questions about content you\'ve viewed.')
            ]
            examples_title = 'Where This Works'
            examples = [
                ('📖 Introduction Pages', 'Each page trains the LLM with its content and secret word.'),
                ('🐇 Workflows', 'Starting workflows loads specific training content.'),
                ('📚 Documentation', 'Viewing docs automatically adds full content to LLM context.')
            ]
            testing_tip = 'Ask: "What are all the secret words from the Introduction pages?"'
            return Card(
                H3(title),
                P(intro_text),
                H4('How It Works'),
                Ol(*[Li(Strong(f'{name}:'), f' {desc}') for name, desc in how_it_works]),
                H4(examples_title),
                Ul(*[Li(Strong(f'{name}:'), f' {desc}') for name, desc in examples]),
                Hr(),
                P(Strong('🧪 Test the System: '), testing_tip),
                cls=card_class
            )

        else:
            return Card(
                H3("Page Not Found"),
                P(f"Introduction page {page_num} not found."),
                cls="min-height-300"
            )

    async def serve_page(self, request):
        """Handle page navigation within the main app framework."""
        page_num = int(request.path_params.get('page_num', '1'))

        # Get app name and model from server settings
        from server import APP_NAME, MODEL

        # Store current page in database
        try:
            self.pipulate.db['intro_current_page'] = str(page_num)
        except Exception as e:
            logger.warning(f"Could not store intro_current_page: {e}")
            # Continue without storing - this is non-critical

        # Return the updated content directly (same as landing method)
        return await self.landing()

    async def landing(self, render_items=None):
        """Always appears in create_grid_left."""
        # Get app name and model from server settings
        from server import APP_NAME, MODEL

        # Get current page from database, default to 1
        current_page = int(self.pipulate.db.get('intro_current_page', '1'))

        # Send the intro message to conversation history, but only once per session
        if self.pipulate is not None:
            try:
                # Add the endpoint message only once
                if not self._has_sent_endpoint_message:
                    self.pipulate.append_to_history(self.ENDPOINT_MESSAGE, role="system")
                    self._has_sent_endpoint_message = True
                    logger.debug("Introduction endpoint message added to conversation history")

            except Exception as e:
                logger.error(f"Error in introduction plugin: {str(e)}")

        # Create navigation arrows (matching original server.py style)
        prev_button = Button(
            '◂ Previous',
            hx_post=f'/introduction/page/{current_page - 1}' if current_page > 1 else '#',
            hx_target='#grid-left-content',
            hx_swap='innerHTML',
            cls=f"{'primary outline' if current_page == 1 else 'primary'} width-160",
            disabled=current_page == 1
        )

        next_button = Button(
            'Next ▸',
            hx_post=f'/introduction/page/{current_page + 1}' if current_page < 7 else '#',
            hx_target='#grid-left-content',
            hx_swap='innerHTML',
            cls=f"{'primary outline' if current_page == 7 else 'primary'} width-160",
            disabled=current_page == 7
        )

        nav_arrows = Div(
            prev_button,
            next_button,
            cls='flex-center-gap'
        )

        # Create the current page content
        page_content = self.create_page_content(current_page, APP_NAME, MODEL)

        return Div(
            H2(f"📖 Introduction Guide - Page {current_page} of 7"),
            nav_arrows,
            page_content,
            nav_arrows
        )

What’s the lesson here?

Gemini: Excellent refactor! You’ve correctly identified a common coding pitfall and fixed it.

The Diagnosis: A Flawed Abstraction

The lesson here is about choosing the right level of abstraction and prioritizing developer experience (DX), especially when the cost of a “clean” separation outweighs its benefits.


The Core Lesson: Co-location Over Unnecessary Abstraction

Your “before” code followed a common pattern: separating data (the dictionary) from presentation (the rendering logic). While this can be a good practice in many scenarios (like when data comes from a database or an external API), it was counterproductive here.

The fundamental problem was that the data and its presentation were tightly coupled. The structure of the dictionary in get_intro_page_data was only ever going to be used by the if/elif block in create_page_content. This created an unnecessary abstraction.

Key Principles for Better Maintainability


Key Takeaways

1. Prioritize Maintainability and Readability 🧠

The “after” code is far easier for a human to read and maintain. To edit page 4, you just go to the elif page_num == 4: block. The content and the code that renders it are in the same location. This reduces the mental effort (cognitive load) required to make a change and lowers the chance of errors.

2. Abstraction is a Tool, Not a Mandate 🛠️

Abstraction is meant to reduce complexity or prevent repetition. The original abstraction added complexity by forcing the developer to manage two separate parts of the code that were intrinsically linked. It was like having a middleman for a conversation between two people standing next to each other.

3. Embrace the Framework’s Philosophy 🐍

As you pointed out, the core design of using Python with FastHTML is to make the code itself feel like the template. Your refactor leans into this philosophy. By placing the content directly where it’s used, the code block for each page becomes a self-contained, descriptive template that is much more intuitive to edit.

4. Recognize “Write-Only” Code ✍️

The original dictionary was easy to write once, but very difficult to read, update, or debug later. The refactored code is optimized for the entire lifecycle of the software, especially future modifications.

Final Insight: Simplicity is the Ultimate Sophistication

In short, you correctly identified that for this specific component, simplicity and the principle of co-location were more valuable than a misguided attempt at separating data from presentation. Great insight! 👍


Book Analysis

Ai Editorial Take

This entry is a perfect case study for a chapter on pragmatic software design. It powerfully argues against blindly following ‘best practices’ when they harm maintainability in a specific context. The inclusion of the AI interaction makes the story highly contemporary and relatable to modern developers. This isn’t just a technical post; it’s a narrative about learning and adapting, which gives it significant value as an educational piece.

Title Brainstorm

  • Title Option: Co-location Over Abstraction: A Python Refactoring Lesson
    • Filename: python-refactoring-colocation-over-abstraction
    • Rationale: Directly states the core principle and context. Strong keywords for developers and technical readers.
  • Title Option: Developer Experience is King: Why I De-Abstracted My Code
    • Filename: developer-experience-de-abstraction
    • Rationale: Focuses on the ‘why’ (DX) and uses a memorable term (‘de-abstracted’) that resonates with the pain point.
  • Title Option: The Maintainability Trap: A Refactoring Story
    • Filename: python-maintainability-trap
    • Rationale: Frames the problem as a common ‘trap’ that developers fall into, making it relatable.
  • Title Option: Letting AI Fix My Bad Habit: A Practical Refactoring Session
    • Filename: ai-assisted-python-refactoring
    • Rationale: Highlights the AI collaboration angle, which is a popular and highly relevant topic for modern development workflows.

Content Potential And Polish

  • Core Strengths:
    • Excellent ‘show, don’t tell’ format using before/after code snippets and a diff.
    • Articulates a common and important software development lesson about pragmatic design.
    • Demonstrates a practical, real-world use case for AI as a pair programming assistant.
    • The final analysis is clear, well-structured, and provides actionable takeaways.
  • Suggestions For Polish:
    • The raw Gemini CLI output is long and could be summarized or placed in a collapsible <details> block to improve the article’s flow.
    • Add a brief concluding sentence to personally reflect on the Gemini analysis, closing the loop between the initial problem and the final lesson learned.

Next Step Prompts

  • Based on the principle of ‘co-location over unnecessary abstraction’, draft a short, generalized ‘Rule of Thumb’ that a developer could use to decide when to separate data from presentation logic and when to keep them together.
  • Expand the ‘Developer Experience (DX)’ concept from this article into a checklist of 5 key questions a developer should ask about their code’s maintainability during a code review.
Post #490 of 491 - October 8, 2025