惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

P
Proofpoint News Feed
博客园_首页
WordPress大学
WordPress大学
大猫的无限游戏
大猫的无限游戏
有赞技术团队
有赞技术团队
阮一峰的网络日志
阮一峰的网络日志
Hugging Face - Blog
Hugging Face - Blog
博客园 - 【当耐特】
酷 壳 – CoolShell
酷 壳 – CoolShell
Y
Y Combinator Blog
Vercel News
Vercel News
The GitHub Blog
The GitHub Blog
T
The Blog of Author Tim Ferriss
云风的 BLOG
云风的 BLOG
博客园 - 司徒正美
Engineering at Meta
Engineering at Meta
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
J
Java Code Geeks
Stack Overflow Blog
Stack Overflow Blog
N
Netflix TechBlog - Medium
Martin Fowler
Martin Fowler
宝玉的分享
宝玉的分享
G
Google Developers Blog
Last Week in AI
Last Week in AI

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Open Source Launch: DocCenter — A Cure for HTML Document ...
LouisQiu · 2026-05-14 · via DEV Community

python aiohttp opensource ai-tools frontend tooling claude chatgpt productivity

Tags:
Suggested platforms: dev.to (best DX) · Hashnode (custom domain) · Medium (broad reach) · X long post


I. The Problem: AI-Era Document Sprawl

For the past year, I've been drowning in AI-generated HTML files.

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

I tried several alternatives and none worked:

Solution Why it didn't work
VSCode Needs a preview plugin; rich-text editing requires switching to source mode
Notion Doesn't accept HTML uploads; copy-paste loses styles
Browser bookmarks Can't edit, can't annotate
Self-hosted static site Too heavy; every change means build → deploy

So I built DocCenter — a local workbench at localhost:9901 purpose-built for this disease.

Repo: https://github.com/louisecxqiu-glitch/html-doc-center


II. Tech Stack: Why a Single Python File + Vanilla JS

DocCenter's entire backend is one server.py, zero requirements.txt, with aiohttp as the only external dependency. The frontend is vanilla JS with no build step.

This isn't showing off — it's intentional. Three key decisions:

2.1 aiohttp over FastAPI

Dimension aiohttp FastAPI
Cold start 0.3s 1.5s (pydantic loading)
Memory ~30MB ~80MB
Mental overhead One web.RouteTableDef and you're done Need to grok Pydantic models

A workbench is not a product. It's a daily-use tool I run on my own laptop. 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.

2.2 Vanilla JS over React

Zero build = zero mental overhead. Fixing a bug doesn't mean npm install → npm run build → refresh. It means edit → Cmd+Shift+R.

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

2.3 iframe over SPA Routing

The HTML files being edited are complete pages — they have their own CSS animations, JS interactions, external fonts. Extracting <body> into an SPA loses all that context.

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


III. Core Architecture: Three Tiers

┌─────────────────────────────────────────────────────────────┐
│  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)          ││
│  └───────────┴──────────────┴──────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘

Enter fullscreen mode Exit fullscreen mode

3.1 Backend (server.py): Path Safety is the Only Hard Rule

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

def _resolve_safe(raw: str, scan_roots: list[str]) -> Optional[Path]:
    """The single gate for path traversal defense."""
    try:
        target = Path(raw).expanduser().resolve()
    except (OSError, RuntimeError):
        return None
    for root in scan_roots:
        root_path = Path(root).expanduser().resolve()
        if target == root_path or root_path in target.parents:
            return target
    return None  # caller returns 403

Enter fullscreen mode Exit fullscreen mode

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

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

3.2 Injection Layer (saver-runtime.js): Three Guardrails for Dirty Detection

This is the hardest part of the project. Dirty detection must trigger only when the user actively edits — not on page JS animations, scroll, or highlight effects.

// Guardrail 1: User interaction window
const USER_INTERACT_WINDOW_MS = 800;
let lastInteract = 0;
['keydown', 'mousedown', 'paste', 'cut', 'drop'].forEach(ev =>
  document.addEventListener(ev, () => { lastInteract = Date.now(); }, true)
);

// Guardrail 2: MutationObserver only watches childList + characterData
const mo = new MutationObserver(mutations => {
  if (Date.now() - lastInteract > USER_INTERACT_WINDOW_MS) return;
  if (mutations.some(m =>
      m.target.tagName === 'SCRIPT' ||
      m.target.tagName === 'STYLE')) return;
  setDirty(true);
});

// Guardrail 3: Delay 1s before observe to skip page init
setTimeout(() => {
  mo.observe(document.body, {
    childList: true,
    characterData: true,
    subtree: true
    // NEVER attributes: true — animations/scroll-highlight cause false positives
  });
}, 1000);

Enter fullscreen mode Exit fullscreen mode

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

3.3 Frontend (app.js): The Single UX Decision Point

When switching files / closing / refreshing while isDirty=true, this dialog appears:

┌─────────────────────────────────────────┐
│  You modified this document             │
│                                         │
│  ✅ Overwrite source                    │
│  🆕 Save as review copy                 │
│  🗑 Discard changes                     │
└─────────────────────────────────────────┘

Enter fullscreen mode Exit fullscreen mode

This is the only decision point in the entire UX. 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. Less is more isn't a slogan; it's a gate every "let's add a button" idea must pass.


IV. 5 Hard-Won Anti-Bug Rules

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 ITERATION-SOP.md:

Rule 1: Real Browser Drill — curl 200 ≠ User-Perspective Working

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

Root cause: CSS .active { display: block } couldn't override inline style="display:none" left over in HTML.

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

Rule 2: Guard Expressions Need Explicit Verification

Cautionary tale (v1.11.11): if (window.sidebarTabsCtl) was forever false because sidebarTabsCtl is an IIFE-internal const and was never attached to window.

// ❌ Forever false
(function() {
  const sidebarTabsCtl = { activate: ... };
})();
if (window.sidebarTabsCtl) { ... }  // never enters

// ✅ Reference within closure
(function() {
  const sidebarTabsCtl = { activate: ... };
  function onClick() {
    sidebarTabsCtl && sidebarTabsCtl.activate('tree');
  }
})();

Enter fullscreen mode Exit fullscreen mode

Rule: Before writing if (X), confirm X's actual visibility in the current scope.

Rule 3: Grep Inline Style Residue Before Changing CSS .active / display

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

Rule 4: DOM-Dependent Actions After Toggle Need rAF

Cautionary tale (v1.11.11): Click favorite folder → activate('tree') toggles display → immediately scrollToPath() calculates position → calculates on stale layout → zero visual feedback.

// ❌ Calculates on stale layout
sidebarTabsCtl.activate('tree');
scrollToPath(path);  // getBoundingClientRect returns stale values

// ✅ Wait for next frame
sidebarTabsCtl.activate('tree');
requestAnimationFrame(() => {
  scrollToPath(path);
});

Enter fullscreen mode Exit fullscreen mode

Rule: Code that reads getBoundingClientRect / scroll / highlight after toggling display/class must use requestAnimationFrame to wait for the next frame.

Rule 5: Autonomous Mode ≠ Skipping User Perspective

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


V. Quick Start & v1.12 Roadmap

Quick Start (3 lines)

git clone https://github.com/louisecxqiu-glitch/html-doc-center.git
cd html-doc-center
pip3 install aiohttp && python3 server.py
# → open http://localhost:9901

Enter fullscreen mode Exit fullscreen mode

macOS auto-start on boot:

cp launchd.plist.example ~/Library/LaunchAgents/com.louis.html-doc-center.plist
launchctl load ~/Library/LaunchAgents/com.louis.html-doc-center.plist

Enter fullscreen mode Exit fullscreen mode

v1.12 Roadmap (in progress)

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

See docs/superpowers/plans/2026-05-14-v1.12-roadmap.md.


VI. Repo & Connect

GitHub: https://github.com/louisecxqiu-glitch/html-doc-center
🐛 Issues / 💡 Discussions / 🔧 PRs all welcome — see CONTRIBUTING.md

Connect:

  • 🐦 X / Twitter: @louisqiu285052 — English build-in-public
  • 📝 CSDN: blog.csdn.net/qcx23 — Chinese deep-dives
  • 🔶 WeChat (Chinese): 一深思AI — companion long-form articles on AI Agent practice

If DocCenter helps you, a star is the best support for open source. Issues and PRs welcome — let's grow this slowly.


Built with ❤️ by Louis Qiu · MIT Licensed · 2026