Compare commits
6 Commits
bf491354e3
...
7a7b4da75e
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a7b4da75e | |||
| 2402566782 | |||
| 47496ac432 | |||
| f98b111193 | |||
| c9b9a3f479 | |||
| d4741e447f |
@@ -34,9 +34,18 @@ Claude writes all sections directly. Be concise, factual, technical. No filler p
|
||||
|
||||
### Filename + append behavior
|
||||
|
||||
- Filename: `YYYY-MM-DD-session.md` (today's local date)
|
||||
- If file exists, **append** a `## Update: HH:MM PT — <topic>` section. Do not overwrite.
|
||||
- If two users worked on the same date, namespace: `YYYY-MM-DD-<user>-<topic>.md` (e.g. `2026-05-01-howard-syncro-billing-batch.md`)
|
||||
**Per-session-unique filenames are mandatory** — 3–4 Claude sessions can run against this one
|
||||
working tree at once, and a shared `YYYY-MM-DD-session.md` lets them overwrite each other's logs.
|
||||
Never use the bare `YYYY-MM-DD-session.md`.
|
||||
|
||||
- Default: `YYYY-MM-DD-<user>-<topic>.md` — `<user>` from the User block (identity.json),
|
||||
`<topic>` a short kebab slug of this session's main work (e.g. `2026-06-05-mike-gururmm-platform-day.md`).
|
||||
The topic naturally separates concurrent sessions.
|
||||
- Collision guard: if that exact filename already exists and belongs to a **different** session
|
||||
(different work), append a discriminator — `YYYY-MM-DD-<user>-<topic>-2.md` (increment until free).
|
||||
Never overwrite another session's file.
|
||||
- Same-session continuation (re-saving your own ongoing work): **append** a
|
||||
`## Update: HH:MM PT — <topic>` section to this session's own file. Do not overwrite.
|
||||
|
||||
### Required sections (in order)
|
||||
|
||||
@@ -77,7 +86,7 @@ Fold what you just worked on into the wiki article so it ships in the **same com
|
||||
- If the synthesis subagent fails or is unavailable, fall back to a surgical **refresh** (bump `last_compiled` + `sources`; refresh client Syncro fields) so the article still records the session, and emit `[WARN] wiki refreshed, not recompiled; run /wiki-compile --full later`.
|
||||
- Any other failure: log it and continue to sync.
|
||||
|
||||
The article + `wiki/index.md` are picked up by `sync.sh`'s `git add -A` and committed alongside the session log.
|
||||
The article + `wiki/index.md` are committed alongside the session log by `sync.sh` (Phase 4).
|
||||
|
||||
---
|
||||
|
||||
@@ -87,7 +96,9 @@ The article + `wiki/index.md` are picked up by `sync.sh`'s `git add -A` and comm
|
||||
bash .claude/scripts/sync.sh
|
||||
```
|
||||
|
||||
`sync.sh` handles: reconcile this machine's `git config user.name/email` to `.claude/identity.json` (so commit authorship can't drift), stage all changes with `git add -A` (after purging garbled Windows path-as-filename cruft), auto-commit, fetch + rebase, push, then the same flow for the vault repo, then surface cross-user `## Note for <user>` blocks.
|
||||
`sync.sh` is **serialized by a per-machine lock** (`.git/claudetools-sync.lock`) so concurrent sessions or the scheduled-task sync cannot interleave commits/rebases; if another sync is mid-flight it waits up to ~120s, then skips (the next sync catches up). It then handles: reconcile this machine's `git config user.name/email` to `.claude/identity.json` (so commit authorship can't drift), stage all changes with `git add -A` (after purging garbled Windows path-as-filename cruft), auto-commit, fetch + rebase, push, then the same flow for the vault repo, then surface cross-user `## Note for <user>` blocks.
|
||||
|
||||
> Note: `git add -A` is still the catch-all sweep, so a save run will also pick up any *other* dirty files in the shared tree. The lock prevents two syncs from racing, and per-session-unique log filenames prevent log overwrites — but the bare-`add -A` capture means full per-session commit isolation is a later step (see the isolation plan: drop blind `add -A` in favour of explicit per-session staging). For now, avoid running `/save` from two sessions at the exact same moment.
|
||||
|
||||
After sync, emit a **Post-commit Summary**:
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
## Feedback
|
||||
- [Bot alerts need a ticket link](feedback_bot_alert_ticket_link.md) — Syncro ticket bot-alerts MUST include a clickable link: https://computerguru.syncromsp.com/tickets/<internal_id> (internal id, not ticket number). post-bot-alert.sh posts raw text; put the URL in the message.
|
||||
- [Verify committed state before push](feedback_verify_committed_state_before_push.md) — webhook builds from origin/main: verify the COMMITTED build (git stash + build), not the working tree; bad git-add pathspec silently aborts staging. Stage by directory.
|
||||
- [Scheduling = coord todo, not schedulers](feedback_scheduling_via_coord_todo.md) — Defer future work as a coord todo (POST /api/coord/todos; needs text + created_by_user + created_by_machine) for a later session to pick up. NOT /schedule remote CCR agents (no vault/creds there) or local scheduled tasks.
|
||||
- [Attribution is read, never inferred](feedback_attribution_from_identity.md) — Who-did-what (user+machine) comes ONLY from identity.json + users.json + git authorship. Never infer from hostname patterns, the userEmail hint, or memory. The "5070" box is Mike's. sync.sh reconciles git config to identity.json; /save renders the User block via whoami-block.sh.
|
||||
- [D2TESTNAS SSH Access](feedback_d2testnas_ssh.md) — Use root@192.168.0.9 with Paper123!@#, not sysadmin.
|
||||
@@ -59,6 +60,7 @@
|
||||
- [Python on Windows — use py launcher](feedback_python_windows.md) — Windows Store python/python3 aliases disabled; always use py or jq on DESKTOP-0O8A1RL.
|
||||
- [Memory tooling may delete now — additive-only constraint dropped](feedback_memory_sync_destructive_ok.md) — As of 2026-06-02, memory-dream and sync-memory.sh are sanctioned to perform destructive ops (apply proposed merges/dedups, propagate repo deletions back to harness profile stores). Onboarding-phase safety net now fights deliberate consolidation (e.g. 2026-06-01's 39 deletions resurrected on the next sync). Script updates pending.
|
||||
- [Unsaved sessions are recoverable from transcripts](feedback_session_recovery.md) — Crashed/closed-before-save sessions live in `~/.claude/projects/<slug>/*.jsonl`; the detector auto-recovers orphans, `/recover <uuid>` does it manually. Ollama prose + Python verbatim. See `.claude/RECOVERY.md`.
|
||||
- [agy review is not read-only](feedback_agy_review_not_readonly.md) — agy review/review-files CAN write files + run npm despite docs claiming plan-mode; always git diff after and treat Gemini's output as a proposal to validate, not trusted/finished work.
|
||||
|
||||
### Syncro
|
||||
- [Syncro API plumbing](feedback_syncro_api.md) — Content-Type required on all POST/PUT; NO idempotency anywhere — always GET before retrying; response wrappers (`.ticket.id`, `.comment.id`); add_line_item shape (internal ID, flat response, required fields); HTML uses `<br>` not `<ul>/<li>`; timer_entry response is FLAT but SUPERSEDED (use add_line_item).
|
||||
|
||||
12
.claude/memory/feedback_agy_review_not_readonly.md
Normal file
12
.claude/memory/feedback_agy_review_not_readonly.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
name: feedback-agy-review-not-readonly
|
||||
description: agy review/review-files can actually WRITE files + run npm, despite docs claiming read-only plan mode — review Gemini's diffs, don't trust its summary.
|
||||
metadata:
|
||||
type: feedback
|
||||
---
|
||||
|
||||
The `agy` SKILL.md documents `review` / `review-files` as read-only (`--approval-mode plan`: "Gemini can read files but cannot modify anything"). Observed 2026-06-05 on GURU-5070: a `review-files` call asking Gemini to "improve" the human-flow skill resulted in Gemini **actually editing 6 repo files, adding babel deps to package.json, and running npm install** (created package-lock.json + node_modules). So plan-mode was NOT enforced for that run.
|
||||
|
||||
**Why:** The documented safety contract (read-only review) cannot be relied on. Gemini also over-claims — its final summary said it "delivered/upgraded" the skill as if complete, but the only way to know what truly happened was to `git diff` and run the code.
|
||||
|
||||
**How to apply:** After ANY `agy review*` call, `git status` / `git diff` the target tree to see what actually changed — never trust the summary. If you need a guaranteed read-only second opinion, copy targets to a scratch dir first, or verify the wrapper's approval-mode. The improvements may be good, but they are a PROPOSAL to review and validate (run it, check repo rules like NO EMOJIS), not trusted output. Related: [[reference_gitea_internal]] is unrelated; see agy SKILL.md path gotcha.
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: feedback_verify_committed_state_before_push
|
||||
description: For webhook-builds-from-main deploys, verify the COMMITTED state builds (not just the working tree); git-add bad-pathspec aborts the whole stage
|
||||
metadata:
|
||||
type: feedback
|
||||
---
|
||||
|
||||
When a deploy pipeline builds from `origin/main` (e.g. GuruRMM's `build-dashboard.sh` does
|
||||
`git reset --hard origin/main` then build), the SERVER builds the COMMITTED content — so a local
|
||||
`tsc`/`vite build` passing against your **working tree** can MASK an incomplete commit and you push a
|
||||
broken main.
|
||||
|
||||
**Why:** A `git add <dir> <deleted-file>` with a stale/deleted pathspec **aborts the entire add**
|
||||
("fatal: pathspec ... did not match"), silently staging nothing — so the commit captured only an
|
||||
earlier `git rm`, not the new files. Working-tree build still passed; the committed build failed on
|
||||
the server. (GuruRMM Phase-2 omnibox, 2026-06-05: main pushed importing a deleted CommandPalette.)
|
||||
|
||||
**How to apply:**
|
||||
- Stage with the DIRECTORY (`git add dashboard/src/components/omnibox`), not the deleted file path.
|
||||
- Before pushing a merge that a webhook will build: verify the **committed** state, e.g.
|
||||
`git stash -u && (cd dashboard && npx tsc -b && npx vite build) ; git stash pop` — or check
|
||||
`git show HEAD:<file>` / `git ls-files <dir>` to confirm the intended files are actually in the commit.
|
||||
- A failed beta build does NOT deploy (marker not written), so beta stays on the last good version —
|
||||
but main is left broken for others until fixed. See [[reference_gururmm]].
|
||||
@@ -1,25 +1,27 @@
|
||||
---
|
||||
name: IX server access — network + SSH
|
||||
description: How to reach ix.azcomputerguru.com (172.16.3.10) — Tailscale-on means it's directly reachable, no separate VPN. SSH currently uses sshpass with the root password (key auth was never set up after GURU-5070 was reinstalled to Windows 11). Setting up key auth would simplify this.
|
||||
description: How to reach ix.azcomputerguru.com (172.16.3.10) — Tailscale-on means it's directly reachable, no separate VPN. SSH KEY AUTH from GURU-5070 now works (verified 2026-06-05); sshpass+password is only the fallback. Also enrolled in GuruRMM (gururmm-agent.service). Full inventory: wiki/systems/ix-server.md.
|
||||
type: reference
|
||||
---
|
||||
|
||||
## Network reachability
|
||||
|
||||
- **Host:** `ix.azcomputerguru.com` / `172.16.3.10`
|
||||
- **Access:** directly reachable when Tailscale is on. No separate VPN connection required.
|
||||
- **Host:** `ix.azcomputerguru.com` / `172.16.3.10` (also `172.16.1.39`)
|
||||
- **Access:** directly reachable when Tailscale is on. No separate VPN connection required. External `72.194.62.5:22` is firewalled — internal only.
|
||||
- **Also enrolled in GuruRMM** (`gururmm-agent.service`, binary `/usr/local/bin/gururmm-agent`, config `/etc/gururmm/agent.toml`) — drivable via `/rmm` when SSH isn't handy.
|
||||
|
||||
## SSH
|
||||
|
||||
> **VERIFY 2026-05-26** — the no-key-auth note was written under the old CachyOS install on GURU-5070; the machine is now Windows 11. Re-confirm whether key auth got set up before relying on the sshpass fallback below.
|
||||
|
||||
- **User:** `root`
|
||||
- **Password:** vault — see `credentials.md` or SOPS.
|
||||
- **SSH key auth:** NOT configured from GURU-5070 (the old `guru@wsl` key was authorized but the workstation was reinstalled; new pubkey hasn't been added to IX's `authorized_keys` yet).
|
||||
- **Current workflow (sshpass):**
|
||||
- **SSH key auth: WORKS from GURU-5070** (verified 2026-06-05 via system OpenSSH, internal IP, Tailscale up):
|
||||
```bash
|
||||
/c/Windows/System32/OpenSSH/ssh.exe -o BatchMode=yes root@172.16.3.10 'whmapi1 listaccts'
|
||||
```
|
||||
- **Password fallback:** vault `infrastructure/ix-server.sops.yaml` (root password). Use sshpass only if key auth ever breaks:
|
||||
```bash
|
||||
sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no -o PubkeyAuthentication=no root@172.16.3.10
|
||||
```
|
||||
- **Suppress sshpass warnings:** pipe through `grep -v WARNING | grep -v 'not using'` or `tail`.
|
||||
- **Account-level (`gurushow`) paths from scripts:** paramiko with `look_for_keys=False, allow_agent=False` (that account's key auth is disabled).
|
||||
|
||||
**Recommended:** add GURU-5070's pubkey to IX's `~/.ssh/authorized_keys` to drop the sshpass dance.
|
||||
## What's on it
|
||||
Full systems inventory (host specs, web/mail/DB stack versions, 72 cPanel accounts → domains → disk, ACG subdomain docroots, backup gap) is documented in **`wiki/systems/ix-server.md`** (live SSH inventory 2026-06-05). cPanel 134, CloudLinux 9.7, 64-core Xeon, 4.4 T /home. [[reference_radio_website]] is hosted here.
|
||||
|
||||
@@ -7,12 +7,14 @@ type: reference
|
||||
## Radio Show Website
|
||||
|
||||
- **URL:** https://radio.azcomputerguru.com
|
||||
- **Platform:** Astro 6.0.4 (static site generator)
|
||||
- **Platform:** Astro 6.0.4 (`output: 'static'`) with **React 19 islands** (`@astrojs/react`), MDX, sitemap, RSS; `wavesurfer.js` (episode audio) + `fuse.js` (client search). Node >= 22.12.0.
|
||||
- **Server:** IX server (172.16.3.10), cPanel account `azcomputerguru`
|
||||
- **Document Root:** `/home/azcomputerguru/public_html/radio`
|
||||
- **Source Code:** `projects/radio-show/website/` in ClaudeTools repo
|
||||
- **Source Code:** `projects/radio-show/website/` in ClaudeTools repo (server holds only built `dist/`)
|
||||
- **Content:** Markdown/MDX collections at `src/content/episodes/` and `src/content/blog/`
|
||||
- **Build:** `cd projects/radio-show/website && npm run build` produces `dist/` folder
|
||||
- **Deploy:** rsync/SCP `dist/` contents to document root on IX server
|
||||
- **Full infra record:** `wiki/systems/ix-server.md`. human-flow can AST-scan the `.tsx` islands under `src/components`, not the `.astro` pages.
|
||||
|
||||
### Community Link
|
||||
- The community page (`/community`) links to:
|
||||
|
||||
@@ -121,6 +121,111 @@ cd "$REPO_ROOT"
|
||||
|
||||
echo -e "${GREEN}[OK]${NC} Working directory: $(pwd)"
|
||||
|
||||
# --- Concurrency lock --------------------------------------------------------
|
||||
# WHY: multiple sync runs on ONE machine must NOT overlap. An interactive /sync
|
||||
# or /save can collide with the scheduled-task sync, or two concurrent Claude
|
||||
# sessions can each stage + commit + fetch + rebase + push and interleave their
|
||||
# git state — corrupting an in-progress rebase, orphaning commits, or pushing a
|
||||
# half-built tree. We serialize the whole claudetools critical section (Phase 1a
|
||||
# submodule update, staging, commit, fetch, rebase, push — and by extension the
|
||||
# vault phase) behind a single per-machine lock.
|
||||
#
|
||||
# PORTABILITY: `flock` is frequently ABSENT on Git Bash (MSYS2), so we can't
|
||||
# depend on it. An atomic `mkdir` is the lowest common denominator — it fails if
|
||||
# the directory already exists, atomically, on every platform we run on (Windows
|
||||
# Git Bash, macOS, Linux). The lock lives under .git/ (never tracked, so a blind
|
||||
# `git add -A` can't stage it) and is scoped to this repo.
|
||||
SYNC_LOCK_DIR="$REPO_ROOT/.git/claudetools-sync.lock"
|
||||
SYNC_LOCK_WAIT=120 # max seconds to wait for a held lock before skipping the run
|
||||
SYNC_LOCK_STALE=600 # seconds after which a held lock is treated as stale (10 min)
|
||||
SYNC_LOCK_OWNED=0 # becomes 1 only once THIS run owns the lock (gates release)
|
||||
|
||||
# Idempotent release — only removes the lock if THIS process actually owns it
|
||||
# (stored PID == $$), so a "skipping this run" exit can never clobber the lock
|
||||
# held by the live sync we deferred to. Installed as an EXIT trap because the
|
||||
# script runs under `set -e`: the lock must be released on error exits too.
|
||||
# (There is no pre-existing EXIT trap in this script, so this adds a fresh one.)
|
||||
release_sync_lock() {
|
||||
if [ "$SYNC_LOCK_OWNED" = "1" ] && [ -d "$SYNC_LOCK_DIR" ]; then
|
||||
local owner_pid
|
||||
owner_pid=$(cat "$SYNC_LOCK_DIR/owner.pid" 2>/dev/null || echo "")
|
||||
if [ -z "$owner_pid" ] || [ "$owner_pid" = "$$" ]; then
|
||||
rm -rf "$SYNC_LOCK_DIR" 2>/dev/null || true
|
||||
fi
|
||||
SYNC_LOCK_OWNED=0
|
||||
fi
|
||||
}
|
||||
trap release_sync_lock EXIT INT TERM
|
||||
|
||||
# Portable liveness check. `kill -0 <pid>` works on Git Bash (it maps to the
|
||||
# Windows process table), macOS, and Linux; guarded so a bad/empty PID is "dead".
|
||||
sync_pid_alive() {
|
||||
local pid="$1"
|
||||
[ -n "$pid" ] || return 1
|
||||
kill -0 "$pid" 2>/dev/null
|
||||
}
|
||||
|
||||
acquire_sync_lock() {
|
||||
local waited=0 owner_pid owner_ts now mtime lock_age stale_aside
|
||||
while true; do
|
||||
if mkdir "$SYNC_LOCK_DIR" 2>/dev/null; then
|
||||
SYNC_LOCK_OWNED=1
|
||||
printf '%s' "$$" > "$SYNC_LOCK_DIR/owner.pid" 2>/dev/null || true
|
||||
# PID + ISO timestamp inside the lock dir, for diagnostics.
|
||||
{
|
||||
printf 'pid=%s\n' "$$"
|
||||
printf 'iso=%s\n' "$(date -u "+%Y-%m-%dT%H:%M:%SZ")"
|
||||
printf 'machine=%s\n' "$MACHINE"
|
||||
} > "$SYNC_LOCK_DIR/owner" 2>/dev/null || true
|
||||
# Defense-in-depth: confirm we still own the dir we just created. If
|
||||
# owner.pid isn't ours, drop ownership and re-evaluate (never fatal
|
||||
# under set -e — comparison is cheap and the body just loops).
|
||||
if [ "$(cat "$SYNC_LOCK_DIR/owner.pid" 2>/dev/null)" != "$$" ]; then
|
||||
SYNC_LOCK_OWNED=0; continue
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
# mkdir failed -> the lock is held. Decide whether it's stale or live.
|
||||
owner_pid=$(cat "$SYNC_LOCK_DIR/owner.pid" 2>/dev/null || echo "")
|
||||
owner_ts=$(sed -n 's/^iso=//p' "$SYNC_LOCK_DIR/owner" 2>/dev/null | head -1)
|
||||
[ -n "$owner_ts" ] || owner_ts="unknown"
|
||||
|
||||
# Stale if the dir is older than the threshold OR the owner PID is dead.
|
||||
# `stat -c` is GNU/Git-Bash, `stat -f` is BSD/macOS; fall back to 0.
|
||||
now=$(date +%s 2>/dev/null || echo 0)
|
||||
mtime=$(stat -c %Y "$SYNC_LOCK_DIR" 2>/dev/null || stat -f %m "$SYNC_LOCK_DIR" 2>/dev/null || echo 0)
|
||||
lock_age=$(( now - mtime ))
|
||||
if { [ "$mtime" -gt 0 ] && [ "$lock_age" -ge "$SYNC_LOCK_STALE" ]; } \
|
||||
|| { [ -n "$owner_pid" ] && ! sync_pid_alive "$owner_pid"; }; then
|
||||
echo -e "${YELLOW}[WARNING]${NC} removing stale sync lock (held by PID ${owner_pid:-?} since ${owner_ts}, age ${lock_age}s)"
|
||||
# Atomically claim the right to clear the stale lock. Only ONE racer can rename
|
||||
# the canonical dir aside (rename source vanishes after the first; the loser's mv
|
||||
# fails and it re-evaluates next pass). The canonical lock name is thereafter only
|
||||
# ever recreated by the atomic mkdir at the top, so a live freshly-acquired lock
|
||||
# can never be rm'd out from under its owner.
|
||||
stale_aside="${SYNC_LOCK_DIR}.stale.$$"
|
||||
if mv "$SYNC_LOCK_DIR" "$stale_aside" 2>/dev/null; then
|
||||
rm -rf "$stale_aside" 2>/dev/null || true
|
||||
fi
|
||||
continue # retry mkdir immediately
|
||||
fi
|
||||
|
||||
# Live lock. If we've waited the full budget, skip (a duplicate sync is
|
||||
# harmless to drop — the next scheduled/interactive run catches up).
|
||||
if [ "$waited" -ge "$SYNC_LOCK_WAIT" ]; then
|
||||
echo -e "${YELLOW}[WARNING]${NC} another sync is in progress (held by PID ${owner_pid:-?} since ${owner_ts}); skipping this run"
|
||||
exit 75 # EX_TEMPFAIL: deferred (another sync in progress), not a real success
|
||||
fi
|
||||
sleep 2
|
||||
waited=$(( waited + 2 ))
|
||||
done
|
||||
}
|
||||
|
||||
acquire_sync_lock
|
||||
echo -e "${GREEN}[OK]${NC} Acquired sync lock ($SYNC_LOCK_DIR)"
|
||||
# --- end concurrency lock ----------------------------------------------------
|
||||
|
||||
# Detect Python interpreter — read from identity.json first, fall back to detection
|
||||
PYTHON=""
|
||||
if [ -f ".claude/identity.json" ] && command -v jq >/dev/null 2>&1; then
|
||||
|
||||
@@ -38,12 +38,24 @@ Run via natural language ("human-flow scan the sessions table", "run human-flow
|
||||
|
||||
| Command | Description |
|
||||
|---------------------|-------------|
|
||||
| `scan [target]` | Quick static + heuristic scan of files or directories for mouse/keyboard friction. Produces a prioritized report. |
|
||||
| `audit [target]` | Deeper pass: combines code analysis, component review, and workflow walkthroughs. Scores intuitiveness and suggests specific refactors. |
|
||||
| `fancy [target]` | **"Fancy as fuck" mode** — a second, beauty- and elegance-focused pass. Evaluates opportunities for tasteful delight (transitions, micro-interactions, hover states, view transitions, loading experiences, etc.), determines appropriateness, and suggests refinements/polish. |
|
||||
| `report [target]` | Generate a clean, user-facing markdown report suitable for sharing with designers/devs. |
|
||||
| `scan [target]` | AST-powered scan of files/directories for workflow friction. Produces a 0-10 Friction Index report. |
|
||||
| `audit [target]` | Deeper pass: combines AST analysis, component review, and state-flow audit. |
|
||||
| `elevate [target]` | **Polish & redesign pass.** Goes beyond friction to make a UI top-notch: information hierarchy, signature moment, action gravity, lonely states, density, rhythm, type, tokens, depth/finish, motion — and flags when a screen should be **redesigned, not patched**. Produces an Elevation Index + prioritized tiers (Quick Wins / Elevations / Redesign Candidates). Add `--redesign` to emphasize structural restructuring. See `references/polish-and-redesign.md`. |
|
||||
| `fix [target]` | **DISABLED (advisory only for now).** Auto-apply is off — the AST code generator reprints whole files and produces noisy diffs. Use the scan/report output and have an agent apply the fixes surgically. Will be revisited with a surgical (string-splice) editor. |
|
||||
| `fancy [target]` | **"Fancy as fuck" mode** — elegance pass with a calibrated Restraint-o-Meter. |
|
||||
| `report [target]` | Generate a formatted markdown report with the Friction Index rubric. |
|
||||
|
||||
If no command, defaults to `scan` on the provided target (or current frontend dir).
|
||||
If no command, defaults to `scan` on the provided target.
|
||||
|
||||
## Friction Index (0-10)
|
||||
|
||||
The scan produces an objective score based on weighted deductions:
|
||||
- **Motor (3.0)**: Target size, precision, Fitts's Law.
|
||||
- **Cognitive (2.5)**: Discoverability, affordance, consistency.
|
||||
- **Keyboard (2.5)**: Accessibility, focus flow, parity.
|
||||
- **Feedback (2.0)**: Visual response, state transitions.
|
||||
|
||||
Score = 10 - Σ(IssueSeverity * DimensionWeight)
|
||||
|
||||
You can combine: e.g. run `scan` first for friction, then `fancy` for delight opportunities.
|
||||
|
||||
@@ -109,6 +121,33 @@ The scanner is **opinionated toward making the happy path for a human operator f
|
||||
|
||||
See `references/report-template.md` for the full structure.
|
||||
|
||||
## "Elevate" Mode (`elevate`) — Polish & Redesign
|
||||
|
||||
Where `scan` finds what *hurts*, `elevate` finds what's *missing to be excellent* — and
|
||||
decides when a screen is beyond polishing and should be **restructured**. It exists because
|
||||
the maintainer is not a designer: after an `elevate` pass, the UI should feel/look/act as if
|
||||
a senior product designer + UI expert + UX team planned it.
|
||||
|
||||
It is primarily an **agent judgment pass** seeded by static signals — read the component,
|
||||
understand the user's task, score each dimension 1–5, then prescribe the concrete better
|
||||
version (a tweak, or a sketched redesign). The 12 heuristics, the scoring model, and the
|
||||
output shape live in `references/polish-and-redesign.md`. In brief:
|
||||
|
||||
- **12 heuristics:** Hierarchy & Visual Anchors · Signature Moment · Action Gravity ·
|
||||
Narrative Coherence · Lonely States (empty/error/loading/success) · Progressive
|
||||
Disclosure & Density · Spacing Rhythm · Typographic Scale · Token Fidelity · Surface/
|
||||
Depth/Finish · Intentional Motion · Redesign Triggers.
|
||||
- **Elevation Index (0–10):** weighted score, with Hierarchy / Signature / Action Gravity /
|
||||
Narrative weighted heaviest.
|
||||
- **Redesign Urgency (0–5):** if ≥ 4, lead with a Structural Audit ("restructure, don't
|
||||
polish") and a sketched alternative layout/component tree.
|
||||
- **Prioritized, not dumped:** `Opportunity = ImpactWeight × (5 − score)`; present the top
|
||||
5–7 as **Quick Wins / Elevations / Redesign Candidates**, each citing file + signal +
|
||||
exact replacement.
|
||||
|
||||
Recommended sequence: `scan` (kill friction) → `elevate` (reach top-notch / decide redesign)
|
||||
→ `fancy` (calibrated delight on top).
|
||||
|
||||
## "Fancy as Fuck" Mode (`fancy`)
|
||||
|
||||
This is a deliberate second (or standalone) pass focused on **beauty, refinement, and elegant interaction**.
|
||||
@@ -146,6 +185,7 @@ The output of a `fancy` pass should live in its own section of the report (or a
|
||||
|
||||
- Add new heuristics to `references/mouse-keyboard-heuristics.md` (with detection hints and "better human workflow" examples).
|
||||
- Add fancy/delights ideas to `references/fancy-as-fuck.md`.
|
||||
- Add polish/redesign heuristics to `references/polish-and-redesign.md` (the `elevate` layer).
|
||||
- Update the scanner script for new static patterns (fancy detection is intentionally more qualitative).
|
||||
- The skill is designed to be extended — new categories of mouse/keyboard friction **and** opportunities for tasteful elegance are welcome.
|
||||
|
||||
|
||||
217
.claude/skills/human-flow/package-lock.json
generated
Normal file
217
.claude/skills/human-flow/package-lock.json
generated
Normal file
@@ -0,0 +1,217 @@
|
||||
{
|
||||
"name": "human-flow",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "human-flow",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@babel/generator": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/traverse": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||
"integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.29.7",
|
||||
"js-tokens": "^4.0.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
|
||||
"integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/types": "^7.29.7",
|
||||
"@jridgewell/gen-mapping": "^0.3.12",
|
||||
"@jridgewell/trace-mapping": "^0.3.28",
|
||||
"jsesc": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-globals": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
|
||||
"integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||||
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
||||
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
|
||||
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
||||
"integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
|
||||
"integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/generator": "^7.29.7",
|
||||
"@babel/helper-globals": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/template": "^7.29.7",
|
||||
"@babel/types": "^7.29.7",
|
||||
"debug": "^4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
|
||||
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.29.7",
|
||||
"@babel/helper-validator-identifier": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jsesc": "bin/jsesc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,5 +6,11 @@
|
||||
"scripts": {
|
||||
"scan": "node scripts/scan.mjs",
|
||||
"fancy": "node scripts/scan.mjs --fancy"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/generator": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/traverse": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,9 +34,24 @@ Before suggesting any fancy element, answer these questions honestly:
|
||||
- Dense internal tools / operator consoles: Favor *restraint and precision*. Think "expensive mechanical instrument" — satisfying, confident, never showy. Over-the-top sparkle or bouncy motion will feel wrong and unprofessional here.
|
||||
- Onboarding, public-facing, marketing, or higher-emotion flows: More permission for expressive, delightful "useful decoration" that makes the experience feel alive and premium while still serving clear user goals.
|
||||
|
||||
## The Restraint-o-Meter
|
||||
|
||||
Calibrate your "Fancy" recommendations using this scale:
|
||||
|
||||
| Level | Profile | Examples |
|
||||
| :--- | :--- | :--- |
|
||||
| **1** | **Clinical** | Zero motion. Immediate cuts. High density. (Log viewers, raw data dumps). |
|
||||
| **2** | **Functional** | Subtle hover states only. (Internal monitoring tools). |
|
||||
| **3** | **Professional** | Standard easings (150-200ms). Skeleton shimmers. (Admin Dashboards, GuruRMM). |
|
||||
| **4** | **Polished** | View Transitions. Subtle card lifts. Optimistic UI. (User-facing settings, consumer tools). |
|
||||
| **5** | **Expressive** | Full shared-element morphs. Physics-based springs. (Onboarding, Marketing). |
|
||||
|
||||
**Guidance**: If you are in an operator console (Level 2-3), avoid any motion that takes > 200ms or that changes element positions significantly.
|
||||
|
||||
---
|
||||
|
||||
## Categories of Elegant Delight
|
||||
## Technical Signals for Fancy Opportunities
|
||||
|
||||
|
||||
### 1. Transitions & Easing (The Foundation)
|
||||
|
||||
|
||||
@@ -193,7 +193,30 @@ Prioritize findings that affect the most frequent user workflows in the product
|
||||
|
||||
---
|
||||
|
||||
## Related Anti-Patterns from Parent Skills
|
||||
## 7. State-Flow Audit (Dynamic Friction)
|
||||
|
||||
**Anti-patterns**:
|
||||
- Elements that jump or shift layout when data loads (layout thrash).
|
||||
- Lack of optimistic UI for frequent, low-risk actions (waiting for server for every checkbox toggle).
|
||||
- "Dead zones" during state transitions where the UI is locked but doesn't look it.
|
||||
|
||||
**Better human workflow**:
|
||||
- Use skeleton screens with consistent dimensions.
|
||||
- Apply optimistic updates with clear rollback on error.
|
||||
- Ensure the "next logical target" is available or signaled as "loading".
|
||||
|
||||
---
|
||||
|
||||
## 8. The Precision Rail & Fumble Zones
|
||||
|
||||
**Anti-patterns**:
|
||||
- Important interactive controls placed in the leftmost 40px or rightmost 40px of a screen with zero padding.
|
||||
- Dense clusters of varied actions in the "Fumble Zone" (corners).
|
||||
|
||||
**Better human workflow**:
|
||||
- Provide at least 16px of "safe padding" on edges.
|
||||
- Group similar actions; keep high-risk actions away from frequent navigation rails.
|
||||
|
||||
|
||||
This skill deliberately overlaps with and specializes rules from `impeccable` (no identical card grids, no hero metrics, strong focus on cognitive load and emotional journey) and `frontend-design` (click targets 44px, hover states, focus states, disabled states).
|
||||
|
||||
|
||||
135
.claude/skills/human-flow/references/polish-and-redesign.md
Normal file
135
.claude/skills/human-flow/references/polish-and-redesign.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# Human-Flow Heuristics: Polish & Redesign (the "Elevate" layer)
|
||||
|
||||
The friction heuristics (`mouse-keyboard-heuristics.md`) find what *hurts*. This layer
|
||||
finds what's *missing to be excellent* — and decides when a screen is beyond polishing
|
||||
and should be **restructured**. Synthesized from three independent model passes (Claude,
|
||||
Gemini, Grok), which converged hard on this set.
|
||||
|
||||
**The bar.** The maintainer is not a designer. After an `elevate` pass, the UI should
|
||||
feel/look/act as if a senior product designer + UI expert + UX team planned it. So this
|
||||
layer is *prescriptive*: don't just flag — propose the concrete better version, and when
|
||||
warranted, sketch a redesign.
|
||||
|
||||
**How to run it.** `elevate` is primarily an **agent judgment pass** seeded by static
|
||||
signals — read the component, understand the user's task, then score and prescribe. The
|
||||
scanner can surface signals (heading counts, raw magic-number styles, missing state
|
||||
branches, animation imports) but the call is the agent's.
|
||||
|
||||
Each heuristic below gives: **what it evaluates** (static signal vs. judgment), the
|
||||
**top-notch bar**, and the **prescription** (the move to recommend — tweak *or* redesign).
|
||||
|
||||
---
|
||||
|
||||
## 1. Hierarchy & Visual Anchors
|
||||
- **Evaluates:** Does visual weight follow importance? *Static:* multiple `<h1>`, repeated identical font-size/weight across unrelated text, div-soup without `section`/`article`/landmark structure, 4+ equally-weighted blocks. *Judgment:* does the dominant thing on screen match the primary user goal?
|
||||
- **Top-notch:** Passes the squint test — one clear primary message, 2–3 supporting levels max, scannable in under 3 seconds without reading every line.
|
||||
- **Prescription:** Consolidate to one `h1` + two supporting levels; promote the key value/data to a large semantic heading, demote metadata to caption/secondary. If 4+ blocks compete, restructure to "primary panel + supporting stack."
|
||||
|
||||
## 2. Signature Moment (First 5 Seconds)
|
||||
- **Evaluates:** Quality of the initial viewport / hero / primary card. *Static:* generic header+content vs. a dedicated orientation block; headline present without supporting microcopy or a primary action. *Judgment:* does it answer "what is this and why act now?"
|
||||
- **Top-notch:** Instant orientation plus a functional or emotional hook — headline + one supporting line + primary action, above the fold, zero ambiguity.
|
||||
- **Prescription:** Replace the generic title with a Signature block (outcome-focused headline, one-sentence value, single primary CTA). On an operator tool, make it a task-oriented "what you can do right now" panel.
|
||||
|
||||
## 3. Action Gravity
|
||||
- **Evaluates:** Is the primary action unmistakable? *Static:* count of primary-styled buttons (>1 is a smell), generic copy ("Submit", "Save"), key actions buried in menus. *Judgment:* is the most important action for *this* context the most salient?
|
||||
- **Top-notch:** Exactly one primary action (or a small, clearly-ranked set), visually and positionally dominant; everything else is secondary/tertiary.
|
||||
- **Prescription:** Elevate the one true primary (size, accent, top-right or sticky action bar). Demote the rest to ghost/icon actions. Rewrite labels to outcome verbs ("Publish changes", "Apply filters"). If two actions are genuinely co-primary for different users, put a segmented choice up front.
|
||||
|
||||
## 4. Narrative Coherence
|
||||
- **Evaluates:** Does the screen tell one logical story matching the task sequence? *Static:* JSX section order vs. logical order, competing CTAs at equal weight, unrelated concerns interleaved. *Judgment:* can a user with the stated goal follow a path without backtracking or "why is this here?"
|
||||
- **Top-notch:** Layout order matches the user's decision/task sequence — one primary thread, clear branches, no random panels.
|
||||
- **Prescription:** Reorder to Context → Decision → Confirmation. Move "related items" into a collapsible tray. If the screen serves two unrelated tasks/roles, split into two focused views.
|
||||
|
||||
## 5. The Lonely States (Empty / Zero / Loading / Error / Success)
|
||||
- **Evaluates:** Are non-happy-path states *designed*? *Static:* conditional rendering with only a happy branch, inline "no data" text, no skeleton/spinner, no `<EmptyState>`/`<ErrorState>`. *Judgment:* does each state reduce anxiety and offer the next action?
|
||||
- **Top-notch:** Every state is designed, not defaulted. Empty states explain *why* and offer the most relevant action; errors are specific + retry + support; success confirms without blocking.
|
||||
- **Prescription:** Add a first-class EmptyState (illustration slot + outcome copy + primary CTA). Replace "no results" with a contextual suggestion (closest useful filter or a create flow). Surface the real error reason + retry, never a bare "something went wrong."
|
||||
|
||||
## 6. Progressive Disclosure & Density Tuning
|
||||
- **Evaluates:** Is density managed, secondary info deferred? *Static:* many visible fields/columns/metrics at once, hover-only details, no accordion/tabs/"show more", long flat tables without grouping. *Judgment:* can the primary task complete without seeing ~80% of the content?
|
||||
- **Top-notch:** Critical path is sparse; secondary detail is one expansion/click away; the user controls the level of detail.
|
||||
- **Prescription:** Collapse the bottom ~60% of a long form into an "Advanced" disclosure. Turn a 12-column table into summary cards + "View details" side panel. For dashboards, add tiered views (Summary / Standard / Full) with a per-user density toggle.
|
||||
|
||||
## 7. Spacing Rhythm & Grid
|
||||
- **Evaluates:** Consistent spatial system? *Static:* hardcoded odd values (`margin: 13px`, `p-[17px]`), inconsistent sibling padding, no spacing scale, cramped containers. *Judgment:* does whitespace group related things and separate unrelated ones (proximity)?
|
||||
- **Top-notch:** Strict 4px/8px grid; related elements closer than unrelated ones; generous, intentional breathing room.
|
||||
- **Prescription:** Normalize all raw px to the nearest spacing token; apply a consistent container `gap`; increase separation between unrelated groups so the eye chunks the layout.
|
||||
|
||||
## 8. Typographic Scale & Readability
|
||||
- **Evaluates:** Is text comfortable and contrasted? *Static:* body < 14px, missing `line-height` on prose, grey-on-grey, raw hex colors vs. WCAG AA. *Judgment:* is long-form content actually pleasant to read?
|
||||
- **Top-notch:** Body 16px+, line-height ~1.5–1.6, a clear primary vs. de-emphasized text system, AA contrast minimum.
|
||||
- **Prescription:** Raise body size/line-height; establish a small type scale (display / heading / body / caption); fix low-contrast pairings to meet AA.
|
||||
|
||||
## 9. System Consistency (Token Fidelity)
|
||||
- **Evaluates:** Do styles come from the system, not one-offs? *Static:* raw numbers in classes (`text-[13px]`, `#3f2a1b`), inline styles, 3 near-identical button/card variants, magic numbers in layout. *Judgment:* justified exception vs. drift?
|
||||
- **Top-notch:** 95%+ of visual decisions come from tokens; new components are *composed*, not invented; rare one-offs are commented.
|
||||
- **Prescription:** Replace raw values with the nearest token. Consolidate near-duplicate components into one with size/emphasis variants. Extract recurring patterns (e.g. section header) into a reusable component that enforces rhythm.
|
||||
|
||||
## 10. Surface, Depth & the Finish Layer (Trust Cues)
|
||||
- **Evaluates:** Does it feel finished and trustworthy? *Static:* no elevation shadows on floating elements, no `:active`/press feedback, generic button text, misaligned numbers, missing timestamps/ownership. *Judgment:* does it feel *crafted* or *assembled*?
|
||||
- **Top-notch:** Subtle shadows convey a Z-axis; interactive elements give a tactile press; numbers right-align; actions read as outcomes; small reassurances ("Last synced 2m ago", "Changes saved automatically") remove doubt.
|
||||
- **Prescription:** Add a consistent surface/chrome to the primary area; `active:` press transform on buttons; right-align numerics; add "last updated" context; unify card/section treatment so widgets read as one product.
|
||||
|
||||
## 11. Intentional Motion & Choreography
|
||||
- **Evaluates:** Does motion serve comprehension, not decoration? *Static:* transition/framer-motion usage without variants, multiple simultaneous transforms on load, >300ms on non-modal elements, no `prefers-reduced-motion`. *Judgment:* does each animation have a purpose (reveal, state change, spatial relationship)?
|
||||
- **Top-notch:** Sparse, purposeful, choreographed; entrances/exits respect spatial relationships; honors reduced-motion. (Calibrate intensity with the Restraint-o-Meter in `fancy-as-fuck.md`.)
|
||||
- **Prescription:** Keep only optimistic state transitions (120–180ms ease), modal/drawer enter-exit with backdrop fade, and at most one staggered reveal that aids scanning. Add a reduced-motion guard. If motion is hiding a weak layout, fix the layout first.
|
||||
|
||||
## 12. Redesign Triggers (Beyond Polishing)
|
||||
- **Evaluates:** Can local polish even fix this? *Static + structural:* >6 competing top-level sections; conditional rendering producing 4+ distinct layouts in one file; component >300 lines or >3 nested ternaries; deep nesting to model a simple hierarchy; "TODO: redesign" comments; a view that accreted 3+ features without re-architecture. *Judgment:* is the screen's conceptual model fundamentally broken?
|
||||
- **Top-notch:** One defensible conceptual model; adding the next feature wouldn't need another special case.
|
||||
- **Prescription:** Declare the redesign threshold crossed. Define the information model first (e.g. "Workspace → Item → Activity"), then a master-detail / two-pane layout that absorbs future features. Provide a sketched component tree + data shape. Do **not** patch the 14-section accordion.
|
||||
|
||||
---
|
||||
|
||||
## Scoring: the Elevation Index
|
||||
|
||||
Score each heuristic **1–5** (1 = absent/harmful, 3 = competent, 5 = senior-designer execution).
|
||||
|
||||
- **Elevation Index** (0–10): weighted average of the 12, scaled to 10. Weight the
|
||||
high-user-impact dimensions heaviest — **Hierarchy, Signature Moment, Action Gravity,
|
||||
Narrative Coherence** (×1.5); the rest ×1.0.
|
||||
- **Redesign Urgency** (0–5): a *separate* score driven mainly by **#12 Redesign
|
||||
Triggers**, reinforced by low **Narrative Coherence** and **Progressive Disclosure**.
|
||||
Urgency ≥ 4 ⇒ lead the report with a **Structural Audit** ("this screen has exceeded
|
||||
patch capacity — restructure, don't polish") and a sketched alternative.
|
||||
|
||||
### Prioritize, don't dump
|
||||
For each finding compute **Opportunity = ImpactWeight × (5 − CurrentScore)**, sort
|
||||
descending, and present the top **5–7** concrete moves (rest in an appendix). Group into
|
||||
three tiers:
|
||||
|
||||
| Tier | Contains | Cost |
|
||||
|---|---|---|
|
||||
| **Quick Wins** | spacing, type, token fidelity, finish details | low effort, high return |
|
||||
| **Elevations** | hierarchy, states, motion, depth, disclosure, action gravity | structural/component-level |
|
||||
| **Redesign Candidates** | Redesign Urgency ≥ 4, or multiple high-impact structural heuristics failing | re-architecture |
|
||||
|
||||
Every recommendation must cite the **file/component**, the **signal that triggered it**,
|
||||
and the **exact replacement pattern or new component/layout shape** — not a vague "improve this."
|
||||
|
||||
---
|
||||
|
||||
## Output shape (elevate)
|
||||
```markdown
|
||||
## Human-Flow Elevate: <target>
|
||||
|
||||
**Elevation Index:** 6.4/10 **Redesign Urgency:** 2/5
|
||||
|
||||
[If Urgency >= 4: Structural Audit block first — why patching won't work + sketched redesign]
|
||||
|
||||
### Quick Wins (do this sprint)
|
||||
1. <file:line> — <signal> -> <concrete token/type/spacing/finish fix>
|
||||
|
||||
### Elevations
|
||||
1. <file:component> — <signal> -> <hierarchy/state/motion/disclosure restructure>
|
||||
|
||||
### Redesign Candidates (plan)
|
||||
1. <file/view> — <triggers> -> <new information model + component tree sketch>
|
||||
|
||||
### Scorecard
|
||||
| Heuristic | Score | Top opportunity |
|
||||
```
|
||||
|
||||
Pair with `scan` (fix friction first) and `fancy` (then calibrated delight). `elevate`
|
||||
is the bridge between "no longer painful" and "genuinely top-notch."
|
||||
@@ -7,12 +7,14 @@ Use this structure for all `scan`, `audit`, and `report` outputs.
|
||||
## Human-Flow Report: <Target / Component / Page>
|
||||
|
||||
**Date**: YYYY-MM-DD
|
||||
**Scanner**: human-flow v1 (mouse + keyboard intuition focus)
|
||||
**Scope**: <files/components scanned>
|
||||
**Overall Human Workflow Score**: X/10
|
||||
- Mouse Ergonomics: X/10
|
||||
- Keyboard Parity & Efficiency: X/10
|
||||
- Workflow Discoverability & Friction: X/10
|
||||
**Scanner**: human-flow v2 (AST-Powered)
|
||||
**Overall Human Workflow Score**: X/10
|
||||
|
||||
### Friction Index Rubric
|
||||
- **Motor (3.0)**: Target size, precision, travel distance.
|
||||
- **Cognitive (2.5)**: Discoverability, affordance, consistency.
|
||||
- **Keyboard (2.5)**: Accessibility, focus flow, parity.
|
||||
- **Feedback (2.0)**: Visual response, state transitions.
|
||||
|
||||
**Summary**
|
||||
(2-4 sentences: the biggest sources of unintuitive behavior for a human operator using mouse and keyboard, and the net effect on daily workflow.)
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* human-flow scanner
|
||||
* human-flow scanner v2 (AST-Powered)
|
||||
*
|
||||
* Static analysis pass for mouse + keyboard workflow friction.
|
||||
* Expands the spirit of frontend-design and impeccable with a narrow,
|
||||
* human-motor-and-expectation focus.
|
||||
* Sophisticated analysis pass for mouse + keyboard workflow friction.
|
||||
* Uses @babel/parser for deep JSX/TSX understanding.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/scan.mjs --path dashboard/src --format json
|
||||
* node scripts/scan.mjs --path dashboard/src/features/sessions
|
||||
*
|
||||
* It is intentionally lightweight (regex + heuristics) so it can run fast
|
||||
* inside agent loops. The real intelligence comes from the agent combining
|
||||
* these findings with full component reading and task-flow understanding.
|
||||
* node scripts/scan.mjs --path src
|
||||
* node scripts/scan.mjs --path src --fix
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { parse } from '@babel/parser';
|
||||
import _traverse from '@babel/traverse';
|
||||
import _generate from '@babel/generator';
|
||||
import * as t from '@babel/types';
|
||||
const traverse = _traverse.default;
|
||||
const generate = _generate.default;
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -25,12 +26,13 @@ const args = process.argv.slice(2);
|
||||
let targetPath = 'src';
|
||||
let format = 'text';
|
||||
let mode = 'friction'; // 'friction' | 'fancy'
|
||||
let applyFix = false;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--path' || args[i] === '-p') targetPath = args[++i];
|
||||
if (args[i] === '--format' || args[i] === '-f') format = args[++i];
|
||||
if (args[i] === '--fancy' || args[i] === '--mode=fancy') mode = 'fancy';
|
||||
if (args[i] === '--mode' && args[i + 1] === 'fancy') { mode = 'fancy'; i++; }
|
||||
if (args[i] === '--fix') applyFix = true;
|
||||
}
|
||||
|
||||
const absTarget = path.resolve(process.cwd(), targetPath);
|
||||
@@ -40,16 +42,40 @@ if (!fs.existsSync(absTarget)) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// `--fix` auto-apply is DISABLED for now: @babel/generator reprints the whole
|
||||
// AST, producing noisy diffs that touch untouched code. Until it does surgical
|
||||
// edits, run advisory only — agents apply fixes surgically from the report.
|
||||
if (applyFix) {
|
||||
console.error('[INFO] --fix (auto-apply) is disabled; running an advisory scan instead. Apply fixes surgically from the report.');
|
||||
applyFix = false;
|
||||
}
|
||||
|
||||
const findings = [];
|
||||
let fixesApplied = 0;
|
||||
|
||||
// Friction Index Rubric Weights
|
||||
const WEIGHTS = {
|
||||
MOTOR: 3.0,
|
||||
COGNITIVE: 2.5,
|
||||
KEYBOARD: 2.5,
|
||||
FEEDBACK: 2.0
|
||||
};
|
||||
|
||||
const SEVERITY_POINTS = {
|
||||
high: 1.0,
|
||||
medium: 0.5,
|
||||
low: 0.2
|
||||
};
|
||||
|
||||
function walk(dir) {
|
||||
if (!fs.existsSync(dir)) return;
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (['node_modules', 'dist', 'build', '.git'].includes(entry.name)) continue;
|
||||
walk(full);
|
||||
} else if (/\.(tsx|jsx|ts|js|css)$/.test(entry.name)) {
|
||||
} else if (/\.(tsx|jsx|ts|js)$/.test(entry.name)) {
|
||||
analyzeFile(full);
|
||||
}
|
||||
}
|
||||
@@ -58,197 +84,256 @@ function walk(dir) {
|
||||
function analyzeFile(file) {
|
||||
const content = fs.readFileSync(file, 'utf8');
|
||||
const rel = path.relative(process.cwd(), file).replace(/\\/g, '/');
|
||||
const lines = content.split('\n');
|
||||
|
||||
if (mode === 'fancy') {
|
||||
// Fancy / beauty & elegance pass — lighter static signals + prompts for qualitative review
|
||||
let match;
|
||||
|
||||
// Existing transitions / animations (look for opportunities to refine)
|
||||
const hasTransition = /transition:|transition-\w+:|animate-|@keyframes|ViewTransition|view-transition/i.test(content);
|
||||
if (hasTransition) {
|
||||
findings.push({
|
||||
file: rel,
|
||||
line: 1,
|
||||
category: 'fancy-existing',
|
||||
severity: 'info',
|
||||
pattern: 'existing-motion',
|
||||
message: 'This file already contains motion/transition code. Good candidate for the fancy pass to review quality, consistency, and restraint.',
|
||||
humanImpact: 'Existing fancy elements can feel either premium or cheap/janky depending on execution.',
|
||||
suggestion: 'In the fancy pass, evaluate easing curves, durations, performance, reduced-motion respect, and whether the motion serves the human workflow or just decorates.'
|
||||
});
|
||||
}
|
||||
|
||||
// Missing View Transitions API in SPA navigation contexts
|
||||
if (/(useNavigate|navigate\(|<Link|react-router|next\/|router\.push)/i.test(content) && !/document\.startViewTransition|View Transitions|view-transition/i.test(content)) {
|
||||
findings.push({
|
||||
file: rel,
|
||||
line: 1,
|
||||
category: 'fancy-opportunity',
|
||||
severity: 'low',
|
||||
pattern: 'missing-view-transitions',
|
||||
message: 'Navigation or view change logic detected without use of the View Transitions API.',
|
||||
humanImpact: 'Page-like changes can feel abrupt or cheap. Modern "ajax-style" smooth transitions between views feel significantly more premium.',
|
||||
suggestion: 'Consider wrapping key navigation with document.startViewTransition() + CSS view-transition-name for elegant morphs or fades. Only where it genuinely improves perceived quality.'
|
||||
});
|
||||
}
|
||||
|
||||
// Basic hover without fancy enhancement
|
||||
if (/:hover\s*\{[^}]*background|transform|box-shadow|scale|opacity/i.test(content)) {
|
||||
findings.push({
|
||||
file: rel,
|
||||
line: 1,
|
||||
category: 'fancy-opportunity',
|
||||
severity: 'low',
|
||||
pattern: 'basic-hover',
|
||||
message: 'Hover state exists but may be basic. Opportunity for more elegant micro-interaction.',
|
||||
humanImpact: 'A merely functional hover feels flat. A refined one (subtle lift + shadow + accent) makes the interface feel alive and high-craft.',
|
||||
suggestion: 'Layer tasteful depth (shadow + slight scale or translate) with excellent easing. Keep it restrained, especially in dense data views.'
|
||||
});
|
||||
}
|
||||
|
||||
return; // In fancy mode we mostly collect signals for the agent to do deep qualitative work
|
||||
}
|
||||
|
||||
// === FRICTION MODE (original) ===
|
||||
|
||||
// 1. Small / sm button targets in interactive contexts (very common friction)
|
||||
const smallButton = /size=["']sm["']|<button[^>]*className=.*btn--sm|height:\s*2[0-8]px|min-height:\s*2[0-8]px/g;
|
||||
let match;
|
||||
while ((match = smallButton.exec(content)) !== null) {
|
||||
const lineNo = content.substring(0, match.index).split('\n').length;
|
||||
findings.push({
|
||||
file: rel,
|
||||
line: lineNo,
|
||||
category: 'target-size',
|
||||
severity: 'high',
|
||||
pattern: 'small-button',
|
||||
message: 'Compact "sm" button or very small height used for an action. Frequent actions (especially in lists) become precision targets.',
|
||||
humanImpact: 'Operators must slow down and aim carefully for common tasks. High error rate under time pressure.',
|
||||
suggestion: 'Use default (md) size for primary/frequent actions. For true compact row actions, ensure generous invisible padding or switch to a larger always-visible treatment.'
|
||||
let modified = false;
|
||||
|
||||
try {
|
||||
const ast = parse(content, {
|
||||
sourceType: 'module',
|
||||
plugins: ['jsx', 'typescript', 'decorators-legacy', 'classProperties'],
|
||||
errorRecovery: true
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Hover-revealed or low-opacity row actions (the classic operator console anti-pattern)
|
||||
if (/\.dt__rowactions|\.rowactions|\.actions\s*\{[^}]*opacity:\s*0\.[0-6]/s.test(content) ||
|
||||
/opacity:\s*0\.[0-6][^}]*hover|hover[^}]*opacity:\s*(1|0\.[7-9])/s.test(content)) {
|
||||
const lineNo = 1; // best effort
|
||||
findings.push({
|
||||
file: rel,
|
||||
line: lineNo,
|
||||
category: 'discoverability',
|
||||
severity: 'high',
|
||||
pattern: 'hover-only-actions',
|
||||
message: 'Row or list actions are dimmed or hidden until hover (or only fully visible on hover).',
|
||||
humanImpact: 'A human scanning a list with eyes + mouse must "paint" every row to discover what they can do. Keyboard users often never see the controls at full strength.',
|
||||
suggestion: 'Raise resting opacity to 0.7–1.0 so actions are scannable at a glance. Or move frequent actions into a dedicated, always-visible column or primary row target. Keep hover only for polish, not discovery.'
|
||||
if (mode === 'fancy') {
|
||||
// Fancy / beauty & elegance pass
|
||||
const hasMotion = /transition:|animate-|@keyframes|framer-motion|ViewTransition/i.test(content);
|
||||
if (hasMotion) {
|
||||
addFinding({
|
||||
file: rel,
|
||||
line: 1,
|
||||
category: 'FEEDBACK',
|
||||
severity: 'low',
|
||||
pattern: 'existing-motion',
|
||||
message: 'Existing motion detected. Review for quality, easing, and restraint.',
|
||||
humanImpact: 'Motion can feel premium or cheap depending on execution.',
|
||||
suggestion: 'Check if easings match the Restraint-o-Meter Level 3-4 (150-250ms).'
|
||||
});
|
||||
}
|
||||
|
||||
// Missing View Transitions in SPA contexts
|
||||
if (/(useNavigate|navigate\(|<Link|router\.push)/i.test(content) && !/document\.startViewTransition/i.test(content)) {
|
||||
addFinding({
|
||||
file: rel,
|
||||
line: 1,
|
||||
category: 'FEEDBACK',
|
||||
severity: 'low',
|
||||
pattern: 'missing-view-transitions',
|
||||
message: 'Navigation detected without View Transitions API.',
|
||||
humanImpact: 'View changes feel abrupt. Transitions feel significantly more premium.',
|
||||
suggestion: 'Wrap navigation in document.startViewTransition() where appropriate.'
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
traverse(ast, {
|
||||
JSXOpeningElement(path) {
|
||||
const node = path.node;
|
||||
const name = getComponentName(node);
|
||||
|
||||
// 1. Unlabeled Icon Button (with Fixer)
|
||||
if (isButtonLike(node) && !hasAriaLabel(node)) {
|
||||
const parent = path.parentPath.node;
|
||||
if (parent.children && hasOnlyIconChild(parent.children)) {
|
||||
if (applyFix) {
|
||||
const iconNode = parent.children.find(c => c.type === 'JSXElement');
|
||||
const iconName = getComponentName(iconNode.openingElement);
|
||||
const label = iconName.replace(/Icon$/, '');
|
||||
node.attributes.push(t.jsxAttribute(t.jsxIdentifier('aria-label'), t.stringLiteral(label)));
|
||||
modified = true;
|
||||
fixesApplied++;
|
||||
} else {
|
||||
addFinding({
|
||||
file: rel,
|
||||
line: node.loc.start.line,
|
||||
category: 'COGNITIVE',
|
||||
severity: 'high',
|
||||
pattern: 'unlabeled-icon-button',
|
||||
message: `Button "${name}" contains only an icon but has no aria-label or title.`,
|
||||
humanImpact: 'Keyboard and screen reader users have no way to know what this button does.',
|
||||
suggestion: 'Add an aria-label or title prop describing the action.'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Tiny Target Calculator
|
||||
if (isInteractive(node)) {
|
||||
const size = getTargetSize(node);
|
||||
if (size < 32) {
|
||||
addFinding({
|
||||
file: rel,
|
||||
line: node.loc.start.line,
|
||||
category: 'MOTOR',
|
||||
severity: 'high',
|
||||
pattern: 'tiny-target',
|
||||
message: `Interactive element "${name}" has a detected size of ~${size}px.`,
|
||||
humanImpact: 'Small targets require high precision, leading to slower workflows and mis-clicks.',
|
||||
suggestion: 'Increase height/width to at least 32px (ideally 44px) or add generous padding.'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Interaction Feedback Missing
|
||||
if (name === 'Button' || name === 'ActionButton') {
|
||||
if (!hasFeedbackProps(node)) {
|
||||
addFinding({
|
||||
file: rel,
|
||||
line: node.loc.start.line,
|
||||
category: 'FEEDBACK',
|
||||
severity: 'medium',
|
||||
pattern: 'missing-feedback-props',
|
||||
message: `Button "${name}" lacks loading or active state props.`,
|
||||
humanImpact: 'Users may be unsure if their click was registered during long operations.',
|
||||
suggestion: 'Add isLoading or active props to provide immediate visual feedback.'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Keyboard Parity: onClick without key handler
|
||||
if (hasProp(node, 'onClick') && !isNativeButton(node) && !hasKeyboardProps(node)) {
|
||||
addFinding({
|
||||
file: rel,
|
||||
line: node.loc.start.line,
|
||||
category: 'KEYBOARD',
|
||||
severity: 'high',
|
||||
pattern: 'click-without-keyboard',
|
||||
message: `Custom element "${name}" has onClick but no keyboard handlers (onKeyDown) or tabIndex.`,
|
||||
humanImpact: 'Keyboard users cannot trigger this action, creating a complete blocker for some workflows.',
|
||||
suggestion: 'Add tabIndex={0} and an onKeyDown handler for Enter/Space.'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 3. onClick without obvious keyboard support on non-native elements
|
||||
const clickNoKeyboard = /onClick=\{[^}]+}\s*(?!.*(onKeyDown|tabIndex|role=))/g;
|
||||
while ((match = clickNoKeyboard.exec(content)) !== null) {
|
||||
const lineNo = content.substring(0, match.index).split('\n').length;
|
||||
// Only flag if it looks like a custom interactive (div, span, custom component in list context)
|
||||
const context = content.substring(Math.max(0, match.index - 80), match.index + 120);
|
||||
if (/<\s*(div|span|tr|td|li|custom|Card|Row)[^>]*onClick|onClick[^>]*<\s*(div|span|tr|td|li|Card|Row)/.test(context)) {
|
||||
findings.push({
|
||||
file: rel,
|
||||
line: lineNo,
|
||||
category: 'keyboard-parity',
|
||||
severity: 'high',
|
||||
pattern: 'click-without-keyboard',
|
||||
message: 'Custom element has onClick but no visible tabIndex/onKeyDown/Enter-Space handling in the immediate area.',
|
||||
humanImpact: 'Keyboard (or mixed mouse+keyboard) users cannot activate the same thing the mouse can without extra workarounds.',
|
||||
suggestion: 'Add tabIndex={0}, onKeyDown handler for Enter/Space, and strong :focus-visible styles. Prefer native <button> when possible.'
|
||||
});
|
||||
if (modified && applyFix) {
|
||||
const output = generate(ast, { retainLines: true }, content);
|
||||
fs.writeFileSync(file, output.code);
|
||||
}
|
||||
} catch (e) {
|
||||
// Graceful degradation: Fallback to regex for critical failures
|
||||
runLegacyRegexScan(content, rel);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Icon-only buttons without accessible name (common with small action icons)
|
||||
const iconButton = /<Button[^>]*>\s*<[^>]+Icon|<\s*button[^>]*>\s*<[^>]+Icon|<[A-Z][^>]*>\s*<[^>]+Icon/g;
|
||||
while ((match = iconButton.exec(content)) !== null) {
|
||||
const lineNo = content.substring(0, match.index).split('\n').length;
|
||||
const nearby = content.substring(Math.max(0, match.index - 30), match.index + 180);
|
||||
if (!/aria-label|title=/.test(nearby)) {
|
||||
findings.push({
|
||||
file: rel,
|
||||
line: lineNo,
|
||||
category: 'discoverability',
|
||||
severity: 'medium',
|
||||
pattern: 'icon-only-no-label',
|
||||
message: 'Icon-only button or action with no aria-label or title.',
|
||||
humanImpact: 'Screen readers and keyboard users (and anyone who forgets what the tiny icon means) have no idea what it does until they activate it.',
|
||||
suggestion: 'Add aria-label (and preferably a visible label or tooltip that works on focus too).'
|
||||
});
|
||||
function addFinding(f) {
|
||||
findings.push(f);
|
||||
}
|
||||
|
||||
// Helpers
|
||||
function getComponentName(node) {
|
||||
if (node.name.type === 'JSXIdentifier') return node.name.name;
|
||||
if (node.name.type === 'JSXMemberExpression') return node.name.property.name;
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function isButtonLike(node) {
|
||||
const name = getComponentName(node);
|
||||
return ['button', 'Button', 'IconButton', 'ActionButton'].includes(name) || hasProp(node, 'role', 'button');
|
||||
}
|
||||
|
||||
function isNativeButton(node) {
|
||||
return getComponentName(node) === 'button';
|
||||
}
|
||||
|
||||
function isInteractive(node) {
|
||||
const name = getComponentName(node);
|
||||
return isButtonLike(node) || ['a', 'input', 'select', 'textarea'].includes(name) || hasProp(node, 'onClick');
|
||||
}
|
||||
|
||||
function hasProp(node, propName, value) {
|
||||
return node.attributes.some(attr => {
|
||||
if (attr.type !== 'JSXAttribute') return false;
|
||||
if (attr.name.name !== propName) return false;
|
||||
if (value === undefined) return true;
|
||||
if (attr.value && attr.value.type === 'StringLiteral') return attr.value.value === value;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function hasAriaLabel(node) {
|
||||
return hasProp(node, 'aria-label') || hasProp(node, 'title') || hasProp(node, 'label');
|
||||
}
|
||||
|
||||
function hasOnlyIconChild(children) {
|
||||
const visibleChildren = children.filter(c => c.type !== 'JSXText' || c.value.trim() !== '');
|
||||
if (visibleChildren.length !== 1) return false;
|
||||
const child = visibleChildren[0];
|
||||
if (child.type !== 'JSXElement') return false;
|
||||
const name = getComponentName(child.openingElement);
|
||||
return name.endsWith('Icon') || name === 'Icon';
|
||||
}
|
||||
|
||||
function getTargetSize(node) {
|
||||
let size = 44; // Default
|
||||
node.attributes.forEach(attr => {
|
||||
if (attr.type === 'JSXAttribute' && attr.name.name === 'size') {
|
||||
if (attr.value.value === 'sm' || attr.value.value === 'xs') size = 28;
|
||||
}
|
||||
}
|
||||
if (attr.type === 'JSXAttribute' && attr.name.name === 'className') {
|
||||
const val = attr.value.value || '';
|
||||
if (val.includes('btn--sm') || val.includes('h-6') || val.includes('h-4')) size = 24;
|
||||
}
|
||||
});
|
||||
return size;
|
||||
}
|
||||
|
||||
// 5. Very narrow status / action columns (precision rail)
|
||||
if (/width:\s*2[0-9]px|width:\s*30px|padding-left:\s*0 !important/.test(content) && /status|actions|select/i.test(content)) {
|
||||
findings.push({
|
||||
function hasFeedbackProps(node) {
|
||||
return hasProp(node, 'loading') || hasProp(node, 'isLoading') || hasProp(node, 'active');
|
||||
}
|
||||
|
||||
function hasKeyboardProps(node) {
|
||||
return hasProp(node, 'onKeyDown') || hasProp(node, 'onKeyPress') || hasProp(node, 'tabIndex');
|
||||
}
|
||||
|
||||
function runLegacyRegexScan(content, rel) {
|
||||
// Simple fallback for files that fail AST parsing
|
||||
if (/onClick=\{[^}]+}\s*(?!.*(onKeyDown|tabIndex|role=))/g.test(content)) {
|
||||
addFinding({
|
||||
file: rel,
|
||||
line: 1,
|
||||
category: 'target-size',
|
||||
severity: 'medium',
|
||||
pattern: 'narrow-rail',
|
||||
message: 'Very narrow column (status, select, or actions rail) used for interactive or important visual elements.',
|
||||
humanImpact: 'Mouse must be extremely precise to hit the control or even read the status comfortably.',
|
||||
suggestion: 'Widen the rail or make the entire left edge a larger hit area (see dt__checkwrap pattern). Status can be visual + text on hover/focus.'
|
||||
});
|
||||
}
|
||||
|
||||
// 6. Row that is fully clickable + internal small actions (mis-click risk)
|
||||
if (/onRowClick|onClick.*row|tr.*onClick/.test(content) && /dt__rowactions|rowactions/.test(content)) {
|
||||
findings.push({
|
||||
file: rel,
|
||||
line: 1,
|
||||
category: 'workflow',
|
||||
severity: 'medium',
|
||||
pattern: 'row-click-plus-internal-actions',
|
||||
message: 'Whole row is clickable (for detail/open) while also containing small action buttons inside the row.',
|
||||
humanImpact: 'Easy to accidentally trigger the row action when aiming for the small icon (or vice versa). Classic source of "I didn\'t mean to open that".',
|
||||
suggestion: 'Make the primary row action very clearly the dominant target (bigger visual weight, different treatment). Or stop making the whole row clickable and use a dedicated primary button + separate secondary actions.'
|
||||
category: 'KEYBOARD',
|
||||
severity: 'high',
|
||||
pattern: 'regex-click-without-keyboard',
|
||||
message: 'Detected onClick without keyboard support via fallback scanner.',
|
||||
humanImpact: 'Potential keyboard blocker.',
|
||||
suggestion: 'Manually review for keyboard parity.'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Start Scan
|
||||
walk(absTarget);
|
||||
|
||||
// Deduplicate similar findings per file
|
||||
const seen = new Set();
|
||||
const uniqueFindings = findings.filter(f => {
|
||||
const key = `${f.file}:${f.pattern}`;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (format === 'json') {
|
||||
console.log(JSON.stringify({ target: absTarget, mode, findings: uniqueFindings }, null, 2));
|
||||
if (applyFix) {
|
||||
console.log(`\nFixed ${fixesApplied} mechanical issues across the target.`);
|
||||
} else {
|
||||
const title = mode === 'fancy'
|
||||
? `Human-Flow "Fancy as Fuck" Signals for: ${absTarget}`
|
||||
: `Human-Flow Scan Results for: ${absTarget}`;
|
||||
|
||||
console.log(`${title}\n`);
|
||||
// Calculate Score
|
||||
const scoreDeductions = findings.reduce((acc, f) => {
|
||||
const dim = f.category;
|
||||
const points = SEVERITY_POINTS[f.severity] * WEIGHTS[dim];
|
||||
acc[dim] = (acc[dim] || 0) + points;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (uniqueFindings.length === 0) {
|
||||
if (mode === 'fancy') {
|
||||
console.log('No obvious static fancy signals detected.\nThis is normal — the real fancy pass is qualitative. Load references/fancy-as-fuck.md and evaluate the target for beauty, elegance, and appropriate delight opportunities.');
|
||||
} else {
|
||||
console.log('No obvious mouse/keyboard friction patterns detected by static rules.\nRun a full agent review with the references/heuristics.md for deeper semantic issues.');
|
||||
}
|
||||
const totalDeduction = Object.values(scoreDeductions).reduce((a, b) => a + b, 0);
|
||||
const finalScore = Math.max(0, Math.min(10, 10 - totalDeduction)).toFixed(1);
|
||||
|
||||
if (format === 'json') {
|
||||
console.log(JSON.stringify({ target: absTarget, score: finalScore, findings }, null, 2));
|
||||
} else {
|
||||
uniqueFindings.forEach((f, i) => {
|
||||
console.log(`${i + 1}. [${f.severity.toUpperCase()}] ${f.category} — ${f.pattern}`);
|
||||
console.log(` File: ${f.file}:${f.line}`);
|
||||
console.log(` ${f.message}`);
|
||||
console.log(` Human impact: ${f.humanImpact}`);
|
||||
console.log(` Suggestion: ${f.suggestion}\n`);
|
||||
});
|
||||
console.log(`## Human-Flow Scan: ${targetPath}`);
|
||||
console.log(`**Overall Human Workflow Score: ${finalScore}/10**\n`);
|
||||
|
||||
if (findings.length === 0) {
|
||||
console.log('[OK] No friction detected. Workflow is clean.');
|
||||
} else {
|
||||
findings.forEach((f, i) => {
|
||||
console.log(`${i + 1}. [${f.severity.toUpperCase()}] ${f.category} — ${f.pattern}`);
|
||||
console.log(` File: ${f.file}:${f.line}`);
|
||||
console.log(` ${f.message}`);
|
||||
console.log(` Human impact: ${f.humanImpact}`);
|
||||
console.log(` Suggestion: ${f.suggestion}\n`);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const exitCode = mode === 'fancy' ? 0 : (uniqueFindings.length > 0 ? 2 : 0);
|
||||
process.exit(exitCode);
|
||||
@@ -644,6 +644,15 @@ export default function PersistentPlayer() {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.persistent-player__progress-bar:focus-visible {
|
||||
outline: 2px solid var(--color-accent, hsl(200 85% 55%));
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.persistent-player__progress-bar:focus-visible .persistent-player__progress-handle {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[data-theme="light"] .persistent-player__progress-bar {
|
||||
background: hsl(220 12% 85% / 0.5);
|
||||
}
|
||||
@@ -755,6 +764,17 @@ export default function PersistentPlayer() {
|
||||
background: hsl(220 16% 92% / 0.6);
|
||||
}
|
||||
|
||||
.persistent-player__btn:focus-visible {
|
||||
outline: 2px solid var(--color-accent, hsl(200 85% 55%));
|
||||
outline-offset: 2px;
|
||||
color: var(--color-text-primary, hsl(220 10% 92%));
|
||||
}
|
||||
|
||||
.persistent-player__btn--play:focus-visible {
|
||||
color: hsl(220 20% 8%);
|
||||
box-shadow: 0 0 16px hsl(200 85% 55% / 0.3);
|
||||
}
|
||||
|
||||
.persistent-player__btn--play {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
@@ -51,12 +51,13 @@ const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<a href="#main-content" class="skip-link">Skip to content</a>
|
||||
<header class="site-header">
|
||||
<nav class="container site-nav">
|
||||
<a href="/" class="site-logo">
|
||||
<span class="logo-text">The Computer Guru Show</span>
|
||||
</a>
|
||||
<div class="nav-links">
|
||||
<div class="nav-links" id="primary-nav">
|
||||
<a href="/episodes">Episodes</a>
|
||||
<a href="/blog">Blog</a>
|
||||
<a href="/live">Live</a>
|
||||
@@ -80,7 +81,7 @@ const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="mobile-menu-toggle" id="mobile-menu-toggle" aria-label="Toggle menu">
|
||||
<button class="mobile-menu-toggle" id="mobile-menu-toggle" aria-label="Toggle menu" aria-expanded="false" aria-controls="primary-nav">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
@@ -88,7 +89,7 @@ const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<main id="main-content" tabindex="-1">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
@@ -134,9 +135,32 @@ const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
||||
});
|
||||
|
||||
// Mobile menu toggle
|
||||
document.getElementById('mobile-menu-toggle').addEventListener('click', () => {
|
||||
document.querySelector('.nav-links').classList.toggle('open');
|
||||
document.getElementById('mobile-menu-toggle').classList.toggle('open');
|
||||
const menuToggle = document.getElementById('mobile-menu-toggle');
|
||||
const navLinks = document.querySelector('.nav-links');
|
||||
|
||||
function closeMobileMenu(returnFocus) {
|
||||
navLinks.classList.remove('open');
|
||||
menuToggle.classList.remove('open');
|
||||
menuToggle.setAttribute('aria-expanded', 'false');
|
||||
if (returnFocus) {
|
||||
menuToggle.focus();
|
||||
}
|
||||
}
|
||||
|
||||
menuToggle.addEventListener('click', () => {
|
||||
const isOpen = navLinks.classList.toggle('open');
|
||||
menuToggle.classList.toggle('open', isOpen);
|
||||
menuToggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
||||
if (isOpen) {
|
||||
const firstLink = navLinks.querySelector('a');
|
||||
if (firstLink) firstLink.focus();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && navLinks.classList.contains('open')) {
|
||||
closeMobileMenu(true);
|
||||
}
|
||||
});
|
||||
|
||||
// Fade-in on scroll
|
||||
|
||||
@@ -31,6 +31,40 @@ a:hover {
|
||||
color: var(--color-accent-hover);
|
||||
}
|
||||
|
||||
/* Global keyboard focus styles — strong, consistent ring on the dark theme.
|
||||
Uses outline so it layers over any existing box-shadow focus rings. */
|
||||
a:focus-visible,
|
||||
button:focus-visible,
|
||||
[tabindex]:focus-visible,
|
||||
input:focus-visible,
|
||||
select:focus-visible,
|
||||
textarea:focus-visible {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 2px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
/* Skip to content link — keyboard-reachable first tab stop, hidden until focused */
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
top: 0;
|
||||
z-index: 1001;
|
||||
padding: var(--space-3) var(--space-6);
|
||||
background: var(--color-bg-secondary);
|
||||
color: var(--color-accent);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
.skip-link:focus {
|
||||
left: var(--space-4);
|
||||
top: var(--space-4);
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
img, video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
|
||||
@@ -70,6 +70,7 @@ Run `/wiki-lint` to check for stale entries and broken backlinks.
|
||||
| [Jupiter](systems/jupiter.md) | 172.16.3.20 — Unraid primary NAS; virsh host for all VMs; Docker: NPM, Gitea, Seafile; iDRAC at 172.16.1.73 | 2026-05-24 |
|
||||
| [Pluto (Claude-Builder)](systems/pluto.md) | 172.16.3.36 — Windows Server 2019 virsh VM on Jupiter; exclusive Windows MSI + cargo build server for GuruRMM | 2026-05-24 |
|
||||
| [Uranus](systems/uranus.md) | 172.16.3.21 — Unraid secondary (Dell R730xd); OwnCloud archive storage only; formerly Saturn's IP (reused Apr 2026); RAM too low for VMs | 2026-05-24 |
|
||||
| [IX Web Hosting Server](systems/ix-server.md) | 172.16.3.10 / 72.194.62.5 — cPanel/WHM 134 on CloudLinux 9.7 (64-core Xeon, 4.4 T /home); **72 cPanel accounts / 185 domains / 101 WordPress** + ACG sites (radio Astro, Flarum community, Matomo analytics); GuruRMM-enrolled; SSH key auth from GURU-5070; behind Cloudflare tunnel `acg-origin`; **backups look unconfigured (gap)**. Live SSH inventory 2026-06-05 — full account→domain map in the article | 2026-06-05 |
|
||||
|
||||
## Patterns
|
||||
|
||||
|
||||
@@ -199,7 +199,7 @@ IX server access: paramiko with `look_for_keys=False, allow_agent=False`. Tailsc
|
||||
## Backlinks
|
||||
|
||||
- `wiki/systems/jupiter.md` [unverified — may not exist yet] — Jupiter server spec
|
||||
- `wiki/systems/ix-server.md` [unverified — may not exist yet] — IX hosting server spec
|
||||
- `wiki/systems/ix-server.md` — IX hosting server + the radio website (Astro) build/deploy record
|
||||
- `wiki/projects/gururmm.md` — related ACG project
|
||||
- `projects/radio-show/audio-processor/README.md` — full pipeline spec and configuration reference
|
||||
- `projects/radio-show/post-show-workflow.md` — full post-show content workflow spec
|
||||
|
||||
233
wiki/systems/ix-server.md
Normal file
233
wiki/systems/ix-server.md
Normal file
@@ -0,0 +1,233 @@
|
||||
---
|
||||
type: system
|
||||
name: ix-server
|
||||
display_name: IX Web Hosting Server
|
||||
last_compiled: 2026-06-05
|
||||
compiled_by: GURU-5070/claude-main
|
||||
sources:
|
||||
- live SSH inventory 2026-06-05 (root@172.16.3.10, key auth from GURU-5070)
|
||||
- .claude/memory/reference_radio_website.md
|
||||
- .claude/memory/reference_ix_server_access.md
|
||||
- wiki/clients/internal-infrastructure.md
|
||||
- projects/radio-show/website/ (source tree)
|
||||
---
|
||||
|
||||
# IX Web Hosting Server
|
||||
|
||||
`ix.azcomputerguru.com` — Arizona Computer Guru's cPanel/WHM shared-hosting box.
|
||||
It hosts ACG's own static sites (incl. **radio.azcomputerguru.com**), the Flarum
|
||||
community forum, Matomo analytics, and ~72 client cPanel accounts (185 domains,
|
||||
101 WordPress installs).
|
||||
|
||||
> Operational history (account cleanups, WordPress hygiene, mail routing, the
|
||||
> Cox→Cloudflare BGP workaround) lives in
|
||||
> [`wiki/clients/internal-infrastructure.md`](../clients/internal-infrastructure.md).
|
||||
> **This article is the systems-level inventory** — what the box is, what runs on
|
||||
> it, and where each hosted thing lives. Last verified by live SSH 2026-06-05.
|
||||
|
||||
## Host facts
|
||||
|
||||
| Item | Value |
|
||||
|---|---|
|
||||
| Hostname | `ix.azcomputerguru.com` |
|
||||
| Internal IP | `172.16.3.10` (also `172.16.1.39`); Tailscale or office LAN required |
|
||||
| External IP | `72.194.62.5` (SSH `:22` firewalled from WAN — internal only) |
|
||||
| OS | CloudLinux 9.7 (Pavel Popovich), kernel `5.14.0 ...tuxcare.els` (TuxCare ELS) |
|
||||
| CPU / RAM | 64× Intel Xeon Gold 6130 @ 2.10 GHz / 62 GiB |
|
||||
| Disk | `/` 70 G (xfs, 50% used); **`/home` 4.4 T (xfs, 1.6 T used, 35%)**; `/boot` + EFI on `sdb` |
|
||||
| Control panel | cPanel/WHM **134.0 (build 35)** |
|
||||
| Web | Apache **2.4.67** (cPanel EA4) |
|
||||
| DB | MariaDB **10.11.16** (per-account MySQL) |
|
||||
| PHP | ea-php **5.6 → 8.5** installed; default CLI **8.1.34** |
|
||||
| Mail | Exim **4.99.4**, Dovecot **2.4.2** |
|
||||
| DNS | BIND **9.16** (104 local zones) |
|
||||
| WHM / cPanel UI | `https://ix.azcomputerguru.com:2087` (WHM) · `:2083` (cPanel) · `:2096` (webmail) — **must stay grey-cloud / DNS-only** in Cloudflare (non-standard ports can't traverse the tunnel) |
|
||||
|
||||
### Access
|
||||
|
||||
| From | How |
|
||||
|---|---|
|
||||
| **GURU-5070** | `ssh root@172.16.3.10` — **SSH key auth works** (verified 2026-06-05). System OpenSSH, internal IP, Tailscale up. |
|
||||
| Scripts to `gurushow` paths | paramiko `look_for_keys=False, allow_agent=False` (account-level key auth disabled) |
|
||||
| Password fallback | Vault `infrastructure/ix-server.sops.yaml` (root password) |
|
||||
| WHM API | `whmapi1 <fn>` over SSH (e.g. `whmapi1 listaccts`) |
|
||||
| RMM | `gururmm-agent.service` is enrolled and running — drive via `/rmm` when SSH isn't handy |
|
||||
|
||||
### Edge / routing
|
||||
|
||||
Cloudflare tunnel **`acg-origin`** (UUID `78d3e58f-1979-4f0e-a28b-98d6b3c3d867`,
|
||||
`cloudflared` container on Jupiter) fronts the ACG hostnames → IX `:443`.
|
||||
`ix.azcomputerguru.com` itself is grey-cloud (direct to `72.194.62.5`) so WHM/cPanel
|
||||
ports route. This tunnel is the workaround for the Cox BGP→Cloudflare routing break
|
||||
(see internal-infrastructure.md).
|
||||
|
||||
### GuruRMM agent
|
||||
|
||||
- Service: `gururmm-agent.service` (active/running), binary `/usr/local/bin/gururmm-agent`
|
||||
- Config: `/etc/gururmm/agent.toml` (root-only, 0600)
|
||||
- Means IX is reachable through the GuruRMM fleet as well as SSH.
|
||||
|
||||
### Backups — gap to watch
|
||||
|
||||
`/backup` exists but is only **~178 M** against a 1.6 T `/home`, and no WHM remote
|
||||
backup-transport destinations are configured. cPanel account backups do **not**
|
||||
appear to be comprehensively enabled/offloaded. [WARNING] Treat IX as **not
|
||||
currently backed up** until verified — flag before any risky account/site work.
|
||||
|
||||
---
|
||||
|
||||
## ACG-owned sites on IX
|
||||
|
||||
The **`azcomputerguru`** cPanel account (`/home/azcomputerguru/public_html/`) hosts
|
||||
ACG's own sites as subdirectories. `gurushow` and `acg` are separate accounts.
|
||||
|
||||
| Hostname | Stack | Document root |
|
||||
|---|---|---|
|
||||
| **radio.azcomputerguru.com** | Astro static + React 19 islands | `.../public_html/radio` |
|
||||
| azcomputerguru.com | Astro static (public site) | `.../public_html` |
|
||||
| community.azcomputerguru.com | Flarum forum | `.../public_html/community/public` |
|
||||
| analytics.azcomputerguru.com | Matomo | `.../public_html/analytics` |
|
||||
| portal / clientportal / clients / support / express / rdb / logos / adlice / khalsaportal .azcomputerguru.com | misc apps/portals | `.../public_html/<name>` |
|
||||
| phoenixmanagedservices.com (+ subdomain) | site | `.../public_html/phoenixmanagedservices.com` |
|
||||
| gurushow.com | radio show brand site | account `gurushow` → `/home/gurushow/public_html` |
|
||||
| (radio audio archive) | static MP3 archive | account `gurushow` → `/home/gurushow/public_html/archive/Radio/` |
|
||||
| acghosting.com | hosting brand | account `acg` |
|
||||
|
||||
(`*.cprapid.com` auto-domains are cPanel temporary URLs — ignore.)
|
||||
|
||||
### radio.azcomputerguru.com — "The Computer Guru Show" website
|
||||
|
||||
Public site for the radio show. **Built by Claude; source in the ClaudeTools repo**
|
||||
(`projects/radio-show/website/`) — the server holds only the built `dist/`. Live
|
||||
build confirmed 2026-06-05 (`<html data-theme="dark">`, dark-mode via localStorage;
|
||||
last deploy ~Mar 2026).
|
||||
|
||||
| Item | Value |
|
||||
|---|---|
|
||||
| URL | https://radio.azcomputerguru.com |
|
||||
| Source | `projects/radio-show/website/` (ClaudeTools repo) |
|
||||
| Framework | Astro `^6.0.4`, `output: 'static'` |
|
||||
| Islands / libs | `@astrojs/react` (React 19), MDX, sitemap, RSS; `wavesurfer.js` (episode audio), `fuse.js` (client search) |
|
||||
| Node | `>= 22.12.0` |
|
||||
| Content | MD/MDX collections `src/content/episodes/`, `src/content/blog/` |
|
||||
| Pages | index, about, community, contact, live, subscribe, episodes/, blog/, feed.xml (RSS), 404 |
|
||||
| Layout | `src/layouts/BaseLayout.astro`; components under `src/components/{global,home,episodes}/` |
|
||||
| Analytics | Matomo site ID 3 — tracker injected into built HTML before `</head>` |
|
||||
|
||||
**Build & deploy** (pure static — no server runtime):
|
||||
```bash
|
||||
cd projects/radio-show/website
|
||||
npm install # first time (node >= 22.12.0)
|
||||
npm run build # -> dist/
|
||||
rsync -az --delete dist/ root@172.16.3.10:/home/azcomputerguru/public_html/radio/
|
||||
```
|
||||
|
||||
It is tier 1/3 of the post-show content workflow in
|
||||
[`wiki/projects/radio-show.md`](../projects/radio-show.md) (episode pages + blog
|
||||
posts generated per episode). The audio pipeline / archive DB / FastAPI UI are
|
||||
separate from this static site.
|
||||
|
||||
**human-flow note:** the AST scanner can analyze the `.tsx` island components under
|
||||
`src/components`; it skips `.astro` page templates (those need a manual/heuristic pass).
|
||||
|
||||
---
|
||||
|
||||
## Client cPanel accounts (72 accounts, 185 domains)
|
||||
|
||||
The "where does client X's website live" map — primary domain per account, sorted by
|
||||
disk. Account home is `/home/<account>/`, web root `/home/<account>/public_html/`.
|
||||
**101 WordPress installs** across these accounts.
|
||||
|
||||
| Disk | Account | Primary domain |
|
||||
|---|---|---|
|
||||
| 85 G | westernt | westerntire.com |
|
||||
| 38 G | azcomputerguru | azcomputerguru.com (ACG — see above) |
|
||||
| 27 G | grabblaw | grabblaw.com |
|
||||
| 25 G | voicesofthewest | voicesofthewest.net |
|
||||
| 18 G | computergurume | computerguru.me |
|
||||
| 8.7 G | acg | acghosting.com (ACG) |
|
||||
| 8.7 G | gurushow | gurushow.com (ACG — radio archive) |
|
||||
| 8.0 G | sparcora | sparcoracing.com |
|
||||
| 7.6 G | azrestaurant | azrestaurantsupply.com |
|
||||
| 7.0 G | acepickupparts | acepickupparts.com |
|
||||
| 6.7 G | perkins | classicsingleaction.com |
|
||||
| 6.1 G | danaise | danaise.com |
|
||||
| 5.3 G | sandtekomachine | sandtekomachinery.com |
|
||||
| 4.4 G | remotesystems | remotesystems.us |
|
||||
| 4.3 G | thegirlsestate | thegirlsestatesales.com |
|
||||
| 3.0 G | upracing | upracing.com |
|
||||
| 2.8 G | farwest | farwestwell.com |
|
||||
| 2.6 G | tucsonadobe | tucsonadobe.com |
|
||||
| 2.5 G | hightechmortgage | hightechmortgage.com |
|
||||
| 2.1 G | bruceext | bruceexterminating.com |
|
||||
| 2.0 G | outaboundssports | outaboundssports.com |
|
||||
| 1.9 G | jrkco | jrkco.com |
|
||||
| 1.9 G | compoundfitness | compoundfunctionalfitness.com |
|
||||
| 1.7 G | ezfastautoglass | ezfastautoglass.com |
|
||||
| 1.5 G | rednourlaw | rednourlaw.com |
|
||||
| 1.4 G | johnmartell | martellappraisals.com |
|
||||
| 1.4 G | peacefulspirit | bestmassageintucson.com |
|
||||
| 1.1 G | rrspc | rrspc.com (active local mail, MX to mail.rrspc.com) |
|
||||
| 1.1 G | nwpool | northwestpoolcare.com |
|
||||
| 898 M | azwebdesign | arizonawebsitedesign.pro |
|
||||
| 835 M | berman | agingwithoutfamily.org |
|
||||
| 819 M | arizonahatters | arizonahatters.com |
|
||||
| 805 M | tucsongc | tucsongoldencorral.com |
|
||||
| 795 M | pcatucson | pcatucson.com |
|
||||
| 774 M | themarcgroup | themarcgroup.com |
|
||||
| 722 M | fsgtucson | fsgtucson.com |
|
||||
| 680 M | antivaxxer | anti-vaxxer.org |
|
||||
| 635 M | desertfox | tucsonpaintball.net |
|
||||
| 604 M | tarafulcreations | tarafulcreations.com |
|
||||
| 583 M | phxpropane | phxpropane.com |
|
||||
| 568 M | gentlemansacres | gentlemansacres.com |
|
||||
| 518 M | rarengineer | rarengineer.com (MX may resolve to IX) |
|
||||
| 492 M | ucryo | ucryo.com |
|
||||
| 479 M | packetdial | packetdial.com |
|
||||
| 358 M | royalweedcontrol | royalweedcontrol.net |
|
||||
| 356 M | drsticken | drsticken.com |
|
||||
| 345 M | photonicapps | photonicapps.com |
|
||||
| 340 M | thrive | thriveappraisalservices.com |
|
||||
| 325 M | cryoweave | cryoweave.com |
|
||||
| 299 M | atlantisbevco | atlantisbevcollc.com |
|
||||
| 267 M | zammit | valleydevelopmentcorp.com |
|
||||
| 258 M | blackswanarchery | blackswanarchery.com |
|
||||
| 242 M | lifelong | lifelonglearningacademy.com |
|
||||
| 239 M | thecentu | thecenturions.com |
|
||||
| 151 M | ultraper | ultraperformance.com |
|
||||
| 80 M | goldench | goldenchoicecatering.com |
|
||||
| 78 M | cascades | cascadestucson.com |
|
||||
| 67 M | jefffurr | jefffurrier.com |
|
||||
| 37 M | lamaddux | lamaddux.com |
|
||||
| 34 M | martylryan | martylryan.com |
|
||||
| 33 M | hoopevents | hoopevents.news |
|
||||
| 27 M | grygutis | barbaragrygutis.com |
|
||||
| 25 M | bardach | bardach.net |
|
||||
| 24 M | wolkin | rswolkin.com |
|
||||
| 20 M | devconllc | devconllc.com |
|
||||
| 19 M | horseshoemgt | horseshoemgt.com |
|
||||
| 16 M | starrpass | starrpass.com |
|
||||
| 16 M | jparkinsonaz | jparkinsonaz.com |
|
||||
| 16 M | heieck | heieck.org |
|
||||
| 8 M | jackfurr | jackfurrier.com |
|
||||
| 8 M | glaztech | glaztech.com |
|
||||
| 3 M | drelenaparra | drelenaparra.com |
|
||||
|
||||
> Several of these map to ACG managed-service clients — e.g. `cryoweave`
|
||||
> (CryoWeave), `rednourlaw` (Rednour Law), `peacefulspirit` (Peaceful Spirit),
|
||||
> `grabblaw` (Grabb & Durando), `ucryo` (Universal Cryogenics), `horseshoemgt`
|
||||
> (Horseshoe Mgmt), `glaztech` (Glaz-Tech), `westernt`/`jackfurr`/`jefffurr`
|
||||
> (Furrier / Western Tire). 185 total domains include addon/parked/subdomains.
|
||||
|
||||
To regenerate this inventory: `ssh root@172.16.3.10` then `whmapi1 listaccts` (full
|
||||
account data), `/etc/trueuserdomains` (primary domain → account), `/etc/userdomains`
|
||||
(all domains → account), `/var/cpanel/userdata/<account>/<domain>` (per-domain docroot).
|
||||
|
||||
---
|
||||
|
||||
## Backlinks
|
||||
|
||||
- [`wiki/clients/internal-infrastructure.md`](../clients/internal-infrastructure.md) — IX operational record (cPanel/WordPress hygiene, mail, Cox/Cloudflare tunnel)
|
||||
- [`wiki/projects/radio-show.md`](../projects/radio-show.md) — radio show project (audio pipeline + post-show workflow)
|
||||
- [`wiki/systems/jupiter.md`](jupiter.md) — runs the `cloudflared` tunnel container fronting IX
|
||||
- Memory: `reference_radio_website.md`, `reference_ix_server_access.md`, `reference_resource_map.md`
|
||||
Reference in New Issue
Block a user