<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: LouisQiu</title>
    <description>The latest articles on DEV Community by LouisQiu (@chuanxi_qiu_dc632f57bdbb6).</description>
    <link>https://dev.to/chuanxi_qiu_dc632f57bdbb6</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3929886%2Fbed092b2-ef34-4db1-b0a5-fcedf79430b6.jpg</url>
      <title>DEV Community: LouisQiu</title>
      <link>https://dev.to/chuanxi_qiu_dc632f57bdbb6</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chuanxi_qiu_dc632f57bdbb6"/>
    <language>en</language>
    <item>
      <title>Open Source Launch: DocCenter — A Cure for HTML Document Sprawl in the AI Era</title>
      <dc:creator>LouisQiu</dc:creator>
      <pubDate>Wed, 13 May 2026 18:17:55 +0000</pubDate>
      <link>https://dev.to/chuanxi_qiu_dc632f57bdbb6/open-source-launch-doccenter-a-cure-for-html-document-sprawl-in-the-ai-era-ji6</link>
      <guid>https://dev.to/chuanxi_qiu_dc632f57bdbb6/open-source-launch-doccenter-a-cure-for-html-document-sprawl-in-the-ai-era-ji6</guid>
      <description>&lt;p&gt;&lt;code&gt;python&lt;/code&gt; &lt;code&gt;aiohttp&lt;/code&gt; &lt;code&gt;opensource&lt;/code&gt; &lt;code&gt;ai-tools&lt;/code&gt; &lt;code&gt;frontend&lt;/code&gt; &lt;code&gt;tooling&lt;/code&gt; &lt;code&gt;claude&lt;/code&gt; &lt;code&gt;chatgpt&lt;/code&gt; &lt;code&gt;productivity&lt;/code&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tags&lt;/strong&gt;: &lt;br&gt;
&lt;strong&gt;Suggested platforms&lt;/strong&gt;: dev.to (best DX) · Hashnode (custom domain) · Medium (broad reach) · X long post&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  I. The Problem: AI-Era Document Sprawl
&lt;/h2&gt;

&lt;p&gt;For the past year, I've been drowning in AI-generated HTML files.&lt;/p&gt;

&lt;p&gt;Claude artifacts: ~20/day. ChatGPT canvas: ~10/day. Cursor and CodeBuddy reports: 5-8/day. They scatter across a dozen folders. &lt;strong&gt;Double-clicking only lets me view; fixing a typo means re-running the original prompt; finding historical versions is impossible.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I tried several alternatives and none worked:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;th&gt;Why it didn't work&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;VSCode&lt;/td&gt;
&lt;td&gt;Needs a preview plugin; rich-text editing requires switching to source mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notion&lt;/td&gt;
&lt;td&gt;Doesn't accept HTML uploads; copy-paste loses styles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser bookmarks&lt;/td&gt;
&lt;td&gt;Can't edit, can't annotate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-hosted static site&lt;/td&gt;
&lt;td&gt;Too heavy; every change means build → deploy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So I built &lt;strong&gt;DocCenter&lt;/strong&gt; — a local workbench at &lt;code&gt;localhost:9901&lt;/code&gt; purpose-built for this disease.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo&lt;/strong&gt;: &lt;a href="https://github.com/louisecxqiu-glitch/html-doc-center" rel="noopener noreferrer"&gt;https://github.com/louisecxqiu-glitch/html-doc-center&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  II. Tech Stack: Why a Single Python File + Vanilla JS
&lt;/h2&gt;

&lt;p&gt;DocCenter's entire backend is &lt;strong&gt;one &lt;code&gt;server.py&lt;/code&gt;, zero &lt;code&gt;requirements.txt&lt;/code&gt;, with &lt;code&gt;aiohttp&lt;/code&gt; as the only external dependency&lt;/strong&gt;. The frontend is vanilla JS with no build step.&lt;/p&gt;

&lt;p&gt;This isn't showing off — it's intentional. Three key decisions:&lt;/p&gt;

&lt;h3&gt;
  
  
  2.1 aiohttp over FastAPI
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;aiohttp&lt;/th&gt;
&lt;th&gt;FastAPI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cold start&lt;/td&gt;
&lt;td&gt;0.3s&lt;/td&gt;
&lt;td&gt;1.5s (pydantic loading)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;~30MB&lt;/td&gt;
&lt;td&gt;~80MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mental overhead&lt;/td&gt;
&lt;td&gt;One &lt;code&gt;web.RouteTableDef&lt;/code&gt; and you're done&lt;/td&gt;
&lt;td&gt;Need to grok Pydantic models&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A workbench is not a product. It's a &lt;strong&gt;daily-use tool I run on my own laptop&lt;/strong&gt;. Fast cold start and low memory beat clean OpenAPI docs by 100×. I have dashboard (9900), heartbeat (4011), cockpit (8088) running simultaneously — I won't accept 80MB per service.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.2 Vanilla JS over React
&lt;/h3&gt;

&lt;p&gt;Zero build = zero mental overhead. Fixing a bug doesn't mean &lt;code&gt;npm install → npm run build → refresh&lt;/code&gt;. It means &lt;strong&gt;edit → Cmd+Shift+R&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The only embedded dependency is &lt;code&gt;marked.min.js&lt;/code&gt; (Markdown rendering, MIT) sitting flat in &lt;code&gt;web/vendor/&lt;/code&gt;. The entire &lt;code&gt;web/&lt;/code&gt; directory has 8 files — that's the whole frontend.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.3 iframe over SPA Routing
&lt;/h3&gt;

&lt;p&gt;The HTML files being edited are &lt;strong&gt;complete pages&lt;/strong&gt; — they have their own CSS animations, JS interactions, external fonts. Extracting &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; into an SPA loses all that context.&lt;/p&gt;

&lt;p&gt;iframe preserves each document's full runtime. DocCenter only injects a small &lt;code&gt;saver-runtime.js&lt;/code&gt; before &lt;code&gt;&amp;lt;/body&amp;gt;&lt;/code&gt;, providing the editing toolbar and auto-save capability. &lt;strong&gt;Keeping the source file's runtime uncontaminated&lt;/strong&gt; has been a hard rule since v1.0.&lt;/p&gt;




&lt;h2&gt;
  
  
  III. Core Architecture: Three Tiers
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────┐
│  Browser at localhost:9901                                  │
│                                                             │
│  ┌─────────────────┐   ┌──────────────────────────────────┐ │
│  │  web/app.js     │   │  iframe                          │ │
│  │  (sidebar tree) │   │  ┌────────────────────────────┐  │ │
│  │                 │←─→│  │ user's HTML                │  │ │
│  │                 │   │  │ + injected saver-runtime.js│  │ │
│  └─────────────────┘   │  └────────────────────────────┘  │ │
│         ↕ HTTP JSON    └──────────────────────────────────┘ │
└─────────┼───────────────────────────────────────────────────┘
          ↓
┌─────────────────────────────────────────────────────────────┐
│  server.py (aiohttp, single file)                           │
│  ┌───────────┬──────────────┬──────────────────────────────┐│
│  │ Static    │ Tree/Config  │ HTML Read/Write              ││
│  │ /         │ /api/tree    │ /api/file (inject saver)     ││
│  │ /static/* │ /api/config  │ /api/snapshot                ││
│  │ /changelog│              │ /api/save (overwrite/new/    ││
│  │           │              │            discard)          ││
│  └───────────┴──────────────┴──────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3.1 Backend (server.py): Path Safety is the Only Hard Rule
&lt;/h3&gt;

&lt;p&gt;Every I/O handler must go through &lt;code&gt;_resolve_safe()&lt;/code&gt;: resolve the input path to absolute, then verify it's under one of the &lt;code&gt;scan_roots&lt;/code&gt;. Otherwise return 403.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_resolve_safe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scan_roots&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;The single gate for path traversal defense.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;expanduser&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;OSError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;scan_roots&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;root_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;expanduser&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;root_path&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;root_path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;  &lt;span class="c1"&gt;# caller returns 403
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;No new I/O handler may bypass this&lt;/strong&gt; — it's been a hard rule since v1.0 and remains unbroken at v1.11.11.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;scan_roots&lt;/code&gt; is configured in &lt;code&gt;~/.codebuddy/html-doc-center/config.json&lt;/code&gt; and editable via the settings panel. Defaults exclude &lt;code&gt;_auto-save / node_modules / .git / dist / build&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.2 Injection Layer (saver-runtime.js): Three Guardrails for Dirty Detection
&lt;/h3&gt;

&lt;p&gt;This is the hardest part of the project. Dirty detection must trigger &lt;strong&gt;only when the user actively edits&lt;/strong&gt; — not on page JS animations, scroll, or highlight effects.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Guardrail 1: User interaction window&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;USER_INTERACT_WINDOW_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;lastInteract&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keydown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mousedown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paste&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cut&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;drop&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;lastInteract&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Guardrail 2: MutationObserver only watches childList + characterData&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MutationObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mutations&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lastInteract&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;USER_INTERACT_WINDOW_MS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mutations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tagName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SCRIPT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tagName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;STYLE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;setDirty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Guardrail 3: Delay 1s before observe to skip page init&lt;/span&gt;
&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;mo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;childList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;characterData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;subtree&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="c1"&gt;// NEVER attributes: true — animations/scroll-highlight cause false positives&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These three guardrails were established after a v1.2.4 false-positive bug. They have not regressed since. &lt;strong&gt;Read the comments before modifying this section&lt;/strong&gt; — it's tempting to "optimize" by enabling &lt;code&gt;attributes: true&lt;/code&gt;, which immediately regresses.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.3 Frontend (app.js): The Single UX Decision Point
&lt;/h3&gt;

&lt;p&gt;When switching files / closing / refreshing while &lt;code&gt;isDirty=true&lt;/code&gt;, this dialog appears:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────┐
│  You modified this document             │
│                                         │
│  ✅ Overwrite source                    │
│  🆕 Save as review copy                 │
│  🗑 Discard changes                     │
└─────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;This is the only decision point in the entire UX.&lt;/strong&gt; I tried adding a 4th 💾 button ("Save and continue editing") in v1.2.5 and reverted it the same hour — more decision points = more user fatigue. &lt;strong&gt;Less is more&lt;/strong&gt; isn't a slogan; it's a gate every "let's add a button" idea must pass.&lt;/p&gt;




&lt;h2&gt;
  
  
  IV. 5 Hard-Won Anti-Bug Rules
&lt;/h2&gt;

&lt;p&gt;42 iterations from v1.0 to v1.11.11 stepped on plenty of mines. The v1.11 series alone had 11 consecutive hotfixes that beat me into submission and produced 5 hard rules, all written into &lt;code&gt;ITERATION-SOP.md&lt;/code&gt;:&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 1: Real Browser Drill — &lt;code&gt;curl 200&lt;/code&gt; ≠ User-Perspective Working
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Cautionary tale (v1.11.10)&lt;/strong&gt;: Three-tab switching feature. &lt;code&gt;curl&lt;/code&gt; returned 200, 0 lint errors, I claimed done. User testing: switching to "Favorites" or "Recent" tab showed blank.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause&lt;/strong&gt;: CSS &lt;code&gt;.active { display: block }&lt;/code&gt; couldn't override inline &lt;code&gt;style="display:none"&lt;/code&gt; left over in HTML.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule&lt;/strong&gt;: Before commit, you must &lt;strong&gt;hard-refresh in browser (Cmd+Shift+R) and click 3+ core interactions from user perspective&lt;/strong&gt;. Acceptance reports cannot consist only of &lt;code&gt;curl 200&lt;/code&gt;. Write "I clicked X in browser and saw Y."&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 2: Guard Expressions Need Explicit Verification
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Cautionary tale (v1.11.11)&lt;/strong&gt;: &lt;code&gt;if (window.sidebarTabsCtl)&lt;/code&gt; was forever false because &lt;code&gt;sidebarTabsCtl&lt;/code&gt; is an IIFE-internal &lt;code&gt;const&lt;/code&gt; and was never attached to &lt;code&gt;window&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Forever false&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sidebarTabsCtl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;activate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sidebarTabsCtl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;// never enters&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Reference within closure&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sidebarTabsCtl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;activate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sidebarTabsCtl&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;sidebarTabsCtl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;activate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tree&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rule&lt;/strong&gt;: Before writing &lt;code&gt;if (X)&lt;/code&gt;, confirm X's actual visibility in the current scope.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 3: Grep Inline Style Residue Before Changing CSS .active / display
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Rule&lt;/strong&gt;: CSS specificity: inline &amp;gt; id &amp;gt; class &amp;gt; tag. Before adding new class-based display control, &lt;strong&gt;&lt;code&gt;grep&lt;/code&gt; old HTML for same-name &lt;code&gt;style="display:none"&lt;/code&gt; residue&lt;/strong&gt; — it will override your CSS. &lt;code&gt;!important&lt;/code&gt; is the last resort, not the first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 4: DOM-Dependent Actions After Toggle Need rAF
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Cautionary tale (v1.11.11)&lt;/strong&gt;: Click favorite folder → &lt;code&gt;activate('tree')&lt;/code&gt; toggles display → immediately &lt;code&gt;scrollToPath()&lt;/code&gt; calculates position → calculates on stale layout → zero visual feedback.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Calculates on stale layout&lt;/span&gt;
&lt;span class="nx"&gt;sidebarTabsCtl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;activate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tree&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;scrollToPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// getBoundingClientRect returns stale values&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Wait for next frame&lt;/span&gt;
&lt;span class="nx"&gt;sidebarTabsCtl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;activate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tree&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;scrollToPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rule&lt;/strong&gt;: Code that reads &lt;code&gt;getBoundingClientRect / scroll / highlight&lt;/code&gt; after toggling &lt;code&gt;display/class&lt;/code&gt; must use &lt;strong&gt;&lt;code&gt;requestAnimationFrame&lt;/code&gt;&lt;/strong&gt; to wait for the next frame.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 5: Autonomous Mode ≠ Skipping User Perspective
&lt;/h3&gt;

&lt;p&gt;When the user says "don't interrupt me with decision cards in autonomous mode," they mean don't send approval prompts — &lt;strong&gt;not&lt;/strong&gt; "skip verification." Every 2-3 versions, run at least one "pretend I'm the user" drill. &lt;strong&gt;The prettier the CHANGELOG user-story section, the more critical to verify in browser&lt;/strong&gt; — otherwise it's documentation-driven self-hypnosis.&lt;/p&gt;




&lt;h2&gt;
  
  
  V. Quick Start &amp;amp; v1.12 Roadmap
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Quick Start (3 lines)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/louisecxqiu-glitch/html-doc-center.git
&lt;span class="nb"&gt;cd &lt;/span&gt;html-doc-center
pip3 &lt;span class="nb"&gt;install &lt;/span&gt;aiohttp &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; python3 server.py
&lt;span class="c"&gt;# → open http://localhost:9901&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;macOS auto-start on boot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp &lt;/span&gt;launchd.plist.example ~/Library/LaunchAgents/com.louis.html-doc-center.plist
launchctl load ~/Library/LaunchAgents/com.louis.html-doc-center.plist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  v1.12 Roadmap (in progress)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Full-text search (FTS5 + debounce)&lt;/li&gt;
&lt;li&gt;Block-level HTML editing (drag-reorder, batch styling)&lt;/li&gt;
&lt;li&gt;Multi-window sync (state broadcast when same file is open in multiple tabs)&lt;/li&gt;
&lt;li&gt;Mobile touch reading mode (drawer-style sidebar)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;See &lt;a href="https://github.com/louisecxqiu-glitch/html-doc-center/blob/main/docs/superpowers/plans/2026-05-14-v1.12-roadmap.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/superpowers/plans/2026-05-14-v1.12-roadmap.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  VI. Repo &amp;amp; Connect
&lt;/h2&gt;

&lt;p&gt;⭐ &lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/louisecxqiu-glitch/html-doc-center" rel="noopener noreferrer"&gt;https://github.com/louisecxqiu-glitch/html-doc-center&lt;/a&gt;&lt;br&gt;
🐛 Issues / 💡 Discussions / 🔧 PRs all welcome — see &lt;a href="https://github.com/louisecxqiu-glitch/html-doc-center/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connect&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🐦 X / Twitter: &lt;a href="https://x.com/louisqiu285052" rel="noopener noreferrer"&gt;@louisqiu285052&lt;/a&gt; — English build-in-public&lt;/li&gt;
&lt;li&gt;📝 CSDN: &lt;a href="https://blog.csdn.net/qcx23" rel="noopener noreferrer"&gt;blog.csdn.net/qcx23&lt;/a&gt; — Chinese deep-dives&lt;/li&gt;
&lt;li&gt;🔶 WeChat (Chinese): 一深思AI — companion long-form articles on AI Agent practice&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If DocCenter helps you, &lt;strong&gt;a star is the best support for open source&lt;/strong&gt;. Issues and PRs welcome — let's grow this slowly.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with ❤️ by Louis Qiu · MIT Licensed · 2026&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>aitools</category>
      <category>vibecoding</category>
    </item>
  </channel>
</rss>
