Skip to content

feat(web): mureo configure Web UI (Phase 1) + official Google Ads provider usability fix#99

Merged
hyoshi merged 35 commits into
mainfrom
feat/web-config-ui-phase1
May 16, 2026
Merged

feat(web): mureo configure Web UI (Phase 1) + official Google Ads provider usability fix#99
hyoshi merged 35 commits into
mainfrom
feat/web-config-ui-phase1

Conversation

@hyoshi
Copy link
Copy Markdown
Collaborator

@hyoshi hyoshi commented May 16, 2026

Summary

Phase 1 of the mureo configure Web UI (Issue #88) plus the mureo providers CLI (Issue #86), culminating in a fix for a blocking bug: the official Google Ads MCP provider registered but was unusable.

The bug (primary deliverable of the latest commit)

Selecting the official Google Ads MCP from the Web UI registered the provider but it could never authenticate:

  1. the wizard Developer-Token + Google OAuth step only ran for the mureo-native choice — picking official skipped credential collection entirely;
  2. the registered mcpServers["google-ads-official"] block had no env, while upstream google-ads-mcp reads config only from environment variables (never ~/.mureo/credentials.json).

Net effect: "✓ registered" then "✗ Failed to connect".

Fix

  • build_credentials_env() — reverse of the closed env-var allow-list → {GOOGLE_ADS_*: value} from credentials.json.
  • extra_env on add_provider_to_claude_settings / add_provider_and_disable_in_mureo via a shared _build_desired_config (idempotent; same single atomic write as the MUREO_DISABLE_* auto-disable).
  • install_provider threads credentials_path and injects env into the registered block for Claude Code and Claude Desktop; Meta hosted_http correctly excluded (browser OAuth on first connect).
  • Wizard: auth runs for native and official Google Ads, ordered before providers_install so credentials exist when the block is written.
  • Credential values redacted from any claude mcp add-json failure message before it can be logged.

Also in this branch

In-progress Phase-1 host-aware work the fix builds on: Claude Code ~/.claude.json MCP registry path, pipx --python sys.executable pin (upstream KeyError: 'Author' crash), Desktop hosted_http manual_required + mureo disable-env, per-platform credentials.json removal endpoint, UI/i18n wiring.

Test plan

  • pytest3133 passed
  • ruff check mureo/ — clean
  • code-reviewer agent: no CRITICAL/HIGH; all MEDIUM items addressed (stderr secret redaction + test, idempotency docstring, wizard ordering invariant comment)
  • Manual: Claude Code → official Google Ads → Developer Token + OAuth → registered block contains populated env → MCP connects

hyoshi added 30 commits May 14, 2026 21:04
Brings C3-C9 web work (handlers, server, oauth_bridge, legacy_commands,
configure_cmd, _data/web/* SPA assets) onto this branch. The external
process committed C1-C9 to feat/core-providers-capabilities by mistake.
C1-C2 were already on this branch under different SHAs (same content,
verified); C3-C9 are now consolidated here as a single commit.

P1-01 changes (mureo/core/, tests/core/) which were mixed in by the
misplaced branch are intentionally excluded — they live on
feat/p1-01-capabilities (PR #90).
Auto-fixes from ruff --fix --unsafe-fixes:
- TC003: move pathlib.Path imports into TYPE_CHECKING blocks
- UP035: use collections.abc.Callable instead of typing.Callable
- black reformatting on 4 files

Behavior unchanged; existing 69 tests still pass.
Recreate 9 test files lost in the 2026-05-14 branch contamination
disaster (FIX-12). The implementation in mureo/web/ survived; only
tests were lost.

268 new tests across:
- test_web_no_framework.py   (19) — AGENTS.md L191 AST scan
- test_web_csrf.py           (44) — session.py CSRF, compare_digest
- test_web_security_headers.py (44) — _helpers.py CSP/XFO/nosniff
- test_web_handlers.py       (40) — route dispatch, Host validation
- test_web_host_paths.py     (18) — Claude Code vs Desktop paths
- test_web_server.py         (25) — ConfigureWizard lifecycle
- test_web_oauth_bridge.py   (25) — WebAuthWizard spawn (mocked)
- test_web_legacy_commands.py (20) — legacy slash command cleanup
- test_web_static_serving.py (33) — path traversal defense

Coverage on mureo/web/: 0% -> 81.91% (meets CI --cov-fail-under=80).
All tests pytest.mark.unit, no real network/subprocess/os.environ.
BUG-1 (discovered by test-writer during test restoration): every
method in handlers.py referenced self.wizard but only self.server.wizard
was set in server.py. Every real HTTP request would crash with
AttributeError on the first wizard access.

Fix: override BaseHTTPRequestHandler.setup() to alias
self.wizard = self.server.wizard before super().setup(). BaseRequestHandler's
__init__ -> setup() -> handle() lifecycle guarantees this runs before
any do_GET / do_POST dispatch, so all 16 self.wizard.* call sites are
healed by the 4-line addition.

Validated end-to-end by 268 new tests in test_web_handlers.py and
test_web_static_serving.py driving real HTTP round-trips against a
real ConfigureWizard on 127.0.0.1:0. The test-writer's
_wire_wizard_onto_handler autouse workaround has been removed.
Four cohesive sub-features for the configure Web UI (Phase 1, #88):

1. Uninstall feature
   - NEW mureo/cli/settings_remove.py: remove_mcp_config,
     remove_credential_guard. Atomic, idempotent, surgical pops only.
     New module to avoid worsening auth_setup.py size.
   - setup_cmd.remove_skills: bundle-driven allow-list, not glob.
   - setup_actions: remove_mureo_mcp/auth_hook/workflow_skills plus
     clear_all_setup orchestrator. Never touches credentials.json.
   - handlers: 4 new CSRF/Host-gated POST routes.
   - Dashboard per-row delete + bulk clear with double-confirm.

2. Legacy --web removal, net -568 LOC
   - Drop the auth setup --web flag and multi-platform browser
     wizard: render_home/after_platform/finish_confirm/done and
     run_web_wizard. WebAuthWizard class kept, oauth_bridge spawns
     it. OAuth completion now redirects to /done.

3. UX fixes from Founder hands-on testing
   - Next button right-aligned on all steps via CSS flex fix.
   - mureo native benefit text on provider-choice step.
   - .btn classes applied to all new buttons; bulk-clear red.
   - SC re-auth skipped when google_ads OAuth already on disk.
   - Top-right close button removed; legacy cross-platform CTA gone.

4. Env panel and locale
   - status_collector env_vars now read from credentials.json,
     was os.environ so wizard inputs never showed. Secret masking
     bullets plus last4, under 8 chars fully masked. Label is now
     mureo credentials.
   - oauth_bridge propagates session locale; web_auth inline i18n.

Tests added: settings_remove 28, setup_cmd_remove 14,
setup_actions_remove 29, status_collector 17. Web suite GREEN.
Pre-commit review verdict: ready_for_user_approval, 0 CRITICAL/HIGH.
Bring the operational demo and byod commands into the single Web UI
surface so non-engineers do not drop to the CLI (Phase 1, #88).

Backend (new modules, mirror setup_actions pattern):
- mureo/web/demo_actions.py: list_demo_scenarios, init_demo with
  _validate_target. Frozen DemoListResult / DemoInitResult.
- mureo/web/byod_actions.py: byod_status / byod_import / byod_remove
  / byod_clear. Frozen ByodResult. _validate_xlsx_path enforces
  absolute path, .xlsx/.xlsm only, realpath traversal defense,
  symlink-escape defeat, NUL/control-char rejection, isfile.
- handlers.py: 6 thin routes. GET /api/demo/scenarios and
  GET /api/byod/status are Host-gated no-CSRF (matches existing GET
  pattern); 4 POST routes through existing Host+CSRF preflight.

Frontend:
- dashboard.js: Demo section (scenario dropdown + target input +
  create) and BYOD section (status table, file-path import,
  per-platform remove, double-confirm clear-all). Reuses .btn
  classes and MUREO.confirmAction / postJson.
- app.html: 2 dashboard-section containers. i18n.json: 33 symmetric
  EN+JA keys.

Local-first: byod uses a server-side file-PATH input, not browser
multipart upload (stdlib http.server, single localhost user).

Tests: demo_actions 34, byod_actions 51, handlers +27. demo_actions
99% / byod_actions 95% coverage. Full web suite 506 GREEN, no
regression. Pre-commit review: ready_for_user_approval, 0 CRITICAL
/HIGH; security path-validation traced and verified.
Founder UX feedback on the configure Web UI Dashboard:

1. Left sidebar navigation - the 7 dashboard sections regrouped
   into 4 nav panes (Setup / Demo / Imported Data / Danger Zone).
   Click or Enter/Space toggles visibility; aria-current on active;
   semantic nav+ul; default = Setup. Section internals unchanged.
2. Term fix - replaced bundle XLSX / Bundle XLSX everywhere with
   the Founder-approved term: JA 広告データファイル (Excel),
   EN Ad data file (Excel). Added a template-to-Excel helper line.
3. Scenario dropdown overflow - select constrained to pane width
   (max-width 100%, box-sizing, ellipsis on closed state).
4. Scenario titles i18n - frontend translation map keyed by
   scenario name (demo.scenario.*), JA titles for the 4 scenarios;
   falls back to API title for unknown scenarios. Python scenario
   files untouched (Web UI scope).

Frontend-static only (no Python, no /api change, no security
boundary). 506 web tests still GREEN. i18n EN/JA parity 134/134.
- .app-main widened to 1200px only when dashboard is active
  (:has([data-dashboard]:not([hidden]))); wizard/landing stay 800px.
  Sidebar 180->210px, content pane absorbs the extra width.
- Scenario <option> now shows the localised title only; the
  hardcoded English sc.blurb (from Python scenario registry, out
  of Web UI scope) is no longer concatenated, so JA locale shows
  fully Japanese scenario names.

Frontend-static only. 506 web tests GREEN.
Founder: pick demo target dir and BYOD Excel file via a native
dialog instead of typing a path. Browsers cannot expose a server-
side absolute path; since the configure UI is single-user
localhost, the server pops a native OS picker.

- NEW mureo/web/native_picker.py: PickResult (frozen),
  pick_directory / pick_file. Spawns sys.executable -c with a
  module-constant tkinter script; argv-only title/patterns,
  shell=False, timeout=300. Cross-platform (macOS + Windows).
  tkinter-missing / cancel / timeout / nonzero all map to a
  PickResult envelope; never raises, server never crashes.
- handlers.py: POST /api/pick/directory + /api/pick/file, thin,
  via existing CSRF+Host preflight.
- dashboard.js: Browse buttons (demo target, BYOD Excel path);
  ok fills input, cancel no-op, error toast + input stays
  editable as fallback.
- BYOD nav + section heading renamed to "BYOD" (EN+JA).
  i18n dashboard.browse / picker_error added, parity preserved.

Picker is convenience only: returned path still validated by
_validate_target / _validate_xlsx_path (cross-platform os.path,
accepts C:\\). Command injection proven impossible (fixed argv).

Tests: native_picker 24, handlers +12. 540 web GREEN, 98%
coverage. Review: ready_for_user_approval, 0 CRITICAL/HIGH.
Browse buttons did nothing on macOS: a Tk dialog spawned from the
localhost server child process is not GUI-activated and stays
behind Finder. Fix: OS-aware picker.

- native_picker.py: darwin uses osascript (macOS builtin) with a
  CONSTANT AppleScript (choose folder / choose file). Client
  title/patterns are IGNORED on macOS = zero injection surface;
  argv fixed [osascript, -e, <const>], shell=False, timeout=300.
  User-cancel (-128 / "User canceled") maps to cancelled, not
  error. Non-darwin keeps the existing tkinter subprocess path
  unchanged. Missing osascript/tkinter still degrades to manual
  entry; never raises. No new deps.
- Removed the "write files only" (skip_import) checkbox from the
  demo menu (Founder: not needed). Backend init_demo still accepts
  the param; UI always sends false. i18n key dropped, EN/JA parity
  preserved.

Tests platform-branched (sys.platform monkeypatched); hostile
title/pattern proven byte-identical in argv. 554 web GREEN,
native_picker 99%. Review: ready_for_user_approval, 0 CRIT/HIGH.
- Header nav gets a persistent Dashboard link (nav.dashboard,
  EN/JA) wired to navigateToDashboard so the user can reach the
  dashboard from the wizard/landing at any time.
- Re-run wizard button now shows ONLY on the Setup nav group;
  hidden on Demo / BYOD / Danger Zone (selectNavGroup toggles it).
- Danger Zone had only the Clear All button with no context;
  added dashboard.clear_all_desc explaining it deletes all mureo
  config (MCP/hook/skills/providers/legacy) and that
  ~/.mureo/credentials.json is NOT touched. EN/JA symmetric.

Frontend-static only. i18n JSON valid, EN/JA parity preserved,
web tests GREEN.
The re-run wizard button stayed visible on Demo/BYOD/Danger
because .btn sets an explicit display, overriding the UA
[hidden]{display:none} rule. Toggle element.style.display
directly in selectNavGroup instead of the hidden attribute.
Buttons with long i18n labels wrapped to multiple lines and grew
vertically. .btn lacked white-space:nowrap.
The demo scenario dropdown (and other JS-built dashboard content:
basic-setup rows, providers, env panel, BYOD status) was rendered
once at boot in the default locale and never rebuilt on language
toggle, because setLocale only re-applies data-i18n attributes —
nodes whose text is set via textContent=MUREO.t(...) were frozen.

app.js already dispatched mureo:locale_changed; added a single
document listener in dashboard.js that calls the existing
renderAll() (guarded to no-op when the dashboard is absent/hidden).
Idempotent: each render clears its container, so en->ja->en keeps
exactly 4 scenario options, no listener leak.

Verified live: boot en, toggle ja -> scenario options become JA
(ハロー効果 / 隠れたチャンピオン / ...), toggle back -> English,
repeat -> stable count 4. 554 web tests GREEN (no backend change).
Found via isolated-HOME round-trip test: selecting the Claude
Desktop host did nothing — setup_actions hardcoded
install_mcp_config(scope=global), always writing the Claude Code
~/.claude/settings.json; claude_desktop_config.json was never
created.

- NEW mureo/web/desktop_mcp.py: surgically writes/removes the
  mcpServers.mureo block in claude_desktop_config.json, reusing
  desktop_installer._load_config/_atomic_write_config/_backup_config.
  Path via get_host_paths (not the macOS-only _macos_config_path).
  Preserves all other mcpServers + unrelated keys; refuses corrupt
  JSON / symlink; never touches ~/.mureo/credentials.json.
- setup_actions: 8 wrappers gain host="claude-code" (default =
  byte-for-byte unchanged Code behavior). Desktop branch: mcp ->
  desktop_mcp; auth_hook -> noop unsupported_on_desktop (Desktop
  config has no hooks surface, writes nothing, part not marked);
  skills host-agnostic. clear_all_setup symmetric for Desktop;
  credentials.json never touched (decision #3 holds).
- handlers: 5 setup POST handlers forward session.host.

Non-macOS Desktop path falls back to ~/.claude/settings.json
(documented Phase-1 limitation; surgical merge preserves any
co-resident Code config — no data loss).

Tests: desktop_mcp 27, setup_actions_host 25, handlers +11.
desktop_mcp 100% coverage. 614 web GREEN, Code back-compat
byte-for-byte. Review: ready_for_user_approval, 0 CRIT/HIGH.
The Desktop mcpServers.mureo block was written as a single
combined string: {"command": "/abs/python -m mureo.mcp"}.
Claude Desktop / the MCP launcher treats command as the
executable and args as a separate array, so a space-joined
string is not a runnable executable — Claude Desktop would fail
to spawn the mureo MCP server. Tests had only asserted the
string was recorded verbatim, so the wrong shape passed review.

Now writes {"command": sys.executable, "args": ["-m",
"mureo.mcp"]}, matching the proven Claude Code _MCP_SERVER_CONFIG
schema. Verified on a real isolated-HOME claude_desktop_config
.json. install_desktop_mcp_block signature gains args:list[str];
surgical-merge / atomic / corrupt+symlink refusal unchanged.
Code-host path byte-for-byte unchanged; credentials.json never
touched. 614 web GREEN.
Bug (Founder Desktop test): selecting Claude Desktop + Meta
official MCP showed registration complete but dashboard kept
meta-ads-official as not-installed, and no Meta OAuth occurred.

Root cause: install_provider/remove_provider had no host param
and always wrote the official MCP into Claude Code\047s
~/.claude/settings.json via config_writer; the provider POST
handlers did not forward session.host (every other setup
handler does). status_collector is already host-aware, so on
Desktop it read claude_desktop_config.json (empty) -> shown as
not installed; Claude Desktop never got the entry. Same class
of bug already fixed for mureo MCP; provider catalog was missed.

- desktop_mcp.py: generic install/remove_desktop_server_block
  (arbitrary server id + block, http or command/args). The
  existing mureo block fns become server_id=mureo wrappers;
  mcpServers.mureo bytes identical (regression-pinned). Catalog
  MappingProxyType deep-unfrozen to plain JSON.
- setup_actions: install_provider/remove_provider gain
  host/home. Code host = verbatim prior behavior. Desktop host
  writes the catalog mcp_server_config block into
  claude_desktop_config.json (pipx/npm subprocess still runs,
  host-agnostic; hosted_http = block only). credentials.json
  never touched. MUREO_DISABLE_* stays Code-only (Phase-2).
- handlers: 2 provider routes forward home/host.
- UI: hosted_http OAuth note (EN/JA) rendered once at wizard
  buildChoiceCard + dashboard renderProvidersSection, clarifying
  Meta official auth happens in Claude on first use (RFC 9728
  MCP OAuth, client-initiated) — configure cannot/should not.

Desktop meta-ads-official block verified == {"type":"http",
"url":"https://mcp.facebook.com/ads"}. 665 web GREEN, Code path
byte-for-byte, mureo block byte-parity. Review: ready, 0 CRIT/HIGH.
Live bug (Founder): starting Claude Desktop after installing
meta-ads-official errored \047claude_desktop_config.json ... not a
valid MCP server configuration ... skipped: meta-ads-official\047.
Claude Desktop config is STDIO-only (command/args); the
{"type":"http","url":...} block is valid only for Claude Code\047s
settings.json (native remote MCP). The prior host-aware fix
wrote the Code-shaped http block verbatim into Desktop config.

Desktop needs the standard mcp-remote npx stdio<->HTTP bridge
(also performs the RFC 9728 OAuth handshake on first connect):
- setup_actions._desktop_block_for(spec): hosted_http ->
  {"command":"npx","args":["-y","mcp-remote",<url>]} (url read
  by key from the trusted catalog, discrete argv element, no
  shell/join); pipx/npm -> spec block verbatim.
- _install_provider_desktop uses it; generic desktop_mcp writer
  UNCHANGED (translation happens before it). Code host path
  byte-for-byte unchanged (still native http block).
- UI: dashboard.provider_desktop_node_note (EN/JA) shown once
  for hosted providers when host=claude-desktop, noting Node
  (npx) is required. mcp-remote is a user-side runtime prereq,
  not a mureo Python dependency (AGENTS.md L191 intact).

Desktop meta-ads-official now == {"command":"npx","args":
["-y","mcp-remote","https://mcp.facebook.com/ads"]}; Code ==
{"type":"http","url":"https://mcp.facebook.com/ads"}. 685 web
GREEN, credentials.json untouched. Review: ready, 0 CRIT/HIGH.
Selecting the OFFICIAL Google Ads MCP from `mureo configure` registered
the provider but left it unable to authenticate:

1. the wizard's Developer-Token + Google OAuth step only ran for the
   mureo-NATIVE provider choice, so picking *official* skipped
   credential collection entirely; and
2. the registered `mcpServers["google-ads-official"]` block carried no
   `env`, while the upstream `google-ads-mcp` reads its config ONLY from
   environment variables, never mureo's `~/.mureo/credentials.json`.

Net effect: "✓ registered" then "✗ Failed to connect".

Fix:
- `build_credentials_env()` — reverse of the per-var writer's closed
  allow-list; materialises {GOOGLE_ADS_*: value} from credentials.json.
- `extra_env` on `add_provider_to_claude_settings` /
  `add_provider_and_disable_in_mureo` via a shared `_build_desired_config`
  (idempotent; merged in the same single atomic write as the
  MUREO_DISABLE_* auto-disable).
- `install_provider` threads `credentials_path` and injects the env into
  the registered block for both Claude Code and Claude Desktop;
  hosted_http (Meta) is excluded — it authenticates via browser OAuth on
  first connect.
- wizard: `auth` step now runs for native AND official Google Ads and is
  ordered before `providers_install` so credentials exist when the block
  is written.
- credential values are redacted from any `claude mcp add-json` failure
  message before it can reach a log line.

This commit also lands the in-progress phase-1 host-aware work the fix
builds on (Claude Code `~/.claude.json` MCP registry path, pipx
`--python sys.executable` pin for the upstream KeyError crash, Desktop
hosted_http manual_required + mureo disable-env, per-platform
credentials.json removal endpoint, UI/i18n wiring).

Tests: 3133 passed; `ruff check mureo/` clean. CHANGELOG updated.
CI mypy flagged the pre-existing port = int(self.server.server_address[1])
(broad typeshed union); the TCPServer this handler runs under always
has the (host, port) tuple. Narrowly scoped ignore, matching the file's
existing convention.
The test computed `expected` outside the platform patch while
`resolved` was inside it; `_claude_desktop_settings_path` is
OS-dependent, so the assertion passed on macOS but failed on the
Linux CI. Compute both sides under the same Darwin patch.
…switch, accurate UI

The five concerns are interdependent within shared files
(setup_actions / config_writer / handlers / i18n / dashboard) and
dependency-coupled, so they ship as one coherent commit rather than
non-green intermediate splits.

1. hosted_http (Meta) is never wired as a raw ~/.claude.json http entry
   (no OAuth DCR → "✗ Failed to connect") and mureo-native Meta is NOT
   auto-disabled (auto-disabling before the official path works strands
   the user). Code, `mureo providers add` CLI, and Desktop all return
   manual_required and point to Claude's account-level Connectors.
   `providers remove` self-heals a stale MUREO_DISABLE_<P>.

2. `mureo providers confirm <id>` + Web "finalize" button: once the
   connector is VERIFIED Connected (timeout-bounded `claude mcp list`
   parse via is_hosted_provider_connected), set MUREO_DISABLE_<P> so the
   model stops calling credential-less native tools. Never before
   verification (no stranding). Desktop → manual.

3. Dashboard "mureo integrations": drop GA4 (mureo has no native GA4 —
   official-only) and add Search Console as a status-only row driven by
   the shared Google OAuth (reflects the wizard's Google sign-in; no
   standalone remove — that stays on the Google Ads row).

4. Dashboard "Official MCP providers" reflects the real hosted-connector
   state via a separate lazily-fetched POST /api/providers/hosted-status
   (kept out of /api/status so it never slows every poll); hosted rows
   show ✓ when Connected and have no Remove button.

5. Host-accurate copy: Connectors steps differ for terminal vs Desktop;
   provider-choice note moved under the OFFICIAL option; host selector
   relabelled "Claude Code (CLI, Desktop app)" /
   "Claude Desktop app (Chat, Cowork)". EN+JA in sync.

Tests: 284 change-set tests pass; ruff+black clean; i18n EN/JA parity.
code-reviewer APPROVE on each increment. CHANGELOG updated.
- Japanese host labels now both use the fullwidth comma 、
  (Claude Code(CLI、デスクトップアプリ)) for consistency.
- The credential-guard hook has no surface on Claude Desktop
  (install_auth_hook → noop:unsupported_on_desktop). The basic-setup
  list in BOTH the wizard and the dashboard now appends
  "(not available on the Desktop app)" /
  "(デスクトップアプリでは利用できません)" to that row only when the
  chosen host is Claude Desktop — never for Claude Code, where the hook
  is installed normally.

i18n EN/JA parity preserved; static-translation (no XSS); code-reviewer
APPROVE. NOT pushed (per request). CHANGELOG updated.
The Japanese 'wizard.step_progress' value still contained the English
word 'Step' ('Step {current} / {total}'), so the progress indicator on
every wizard step — including the Setup complete screen — rendered
English in the JA locale. Now 'ステップ {current} / {total}'. EN
unchanged; placeholders preserved; EN/JA key parity intact.
code-reviewer APPROVE. Not pushed.
The auth wizard's GA4 step rendered service-account-path / project-id
inputs and a Done button whose handler only called onAllDone() — the
values were never saved. GOOGLE_APPLICATION_CREDENTIALS /
GOOGLE_PROJECT_ID were therefore never written to credentials.json and
ga4-official launched unauthenticated (same class as the earlier
Google Ads bug).

Done now POSTs each value through the allow-listed
/api/credentials/env-var writer (ga4 section) BEFORE advancing; it only
proceeds if every write succeeds, otherwise it shows a save-failed
message and stays on the step. Slot inputs now carry the env-var name +
a localized label key; saving/failure status + GA4 labels added EN+JA.

Test: test_code_host_injects_ga4_env_from_credentials pins the
end-to-end contract (ga4 creds → install injects them into the
ga4-official block). Full suite 3158 passed; ruff+black clean;
code-reviewer APPROVE. Not pushed.
The dashboard "Claude app / 設定先" field rendered the raw host id
(`claude-desktop` / `claude-code`). It now maps to the same friendly
labels as the wizard host selector (reusing the existing
wizard.host.* i18n keys, no new keys): claude-desktop → "Claude
Desktop app (Chat, Cowork)" / "Claudeデスクトップアプリ(Chat、Cowork)",
claude-code → "Claude Code (CLI, Desktop app)" /
"Claude Code(CLI、デスクトップアプリ)". Sets data-i18n so a locale
switch re-resolves; unknown/empty host falls back to the raw value
with data-i18n removed.

code-reviewer APPROVE. Not pushed.
buildStepBody_completed built its title/description/platform-summary/
dashboard-button via build-time MUREO.t() with no data-i18n attribute.
The locale switcher (app.js setLocale → applyTranslations) only
re-resolves [data-i18n] nodes, so switching language after the
completed screen rendered left those nodes frozen in the build-time
locale (English) while every other data-i18n screen updated — the
reported "Setup complete stays English". (pending_meta already had
data-i18n and updated, confirming the diagnosis.)

Added data-i18n to the title h2, desc p, each platform <li>, and the
dashboard button (existing keys reused; none added). code-reviewer
APPROVE. Not pushed.
hyoshi added 5 commits May 16, 2026 14:31
Bundled (interdependent, on the unmerged web-config PR; shared files —
setup_actions / CHANGELOG / i18n — make a finer split produce non-green
intermediate commits):

- Backfill: install_mureo_mcp (basic-setup AND dashboard path) now sets
  MUREO_DISABLE_<P> for already-registered pipx/npm official providers
  (google/ga4) so "official added first, mureo MCP later" no longer
  leaves native+official both active with no precedence. Pure file
  read; Meta deliberately excluded (no network probe on basic setup —
  Meta stays the explicit confirm / toggle path). Best-effort,
  idempotent, never invents a mureo block.
- Web UI per-platform native↔official toggle: status.mureo_disable
  exposes per-platform state; POST /api/providers/native-toggle
  sets/unsets MUREO_DISABLE_<P>. Switching TO official is allowed only
  when the official path is in effect (pipx/npm registered, or Meta
  connector verified Connected) — refused with an actionable detail;
  switching back to native is always allowed (un-strand). Host-aware
  (Code + Desktop), EN+JA, restart-to-apply note.
- _apply_disable_env: single host-dispatch/env-var-name helper shared
  by backfill and the toggle (removes duplicated logic).
- /done OAuth-wizard page: was a hardcoded English constant ignoring
  session locale; now render_done_page(locale) via _t + _html_lang,
  done.* keys EN+JA.
- UI polish: GA4 has no native tools → its toggle is not rendered;
  colored ✓ (green) / ✗ (red) status marks (own span so a locale
  re-translation can't wipe them); spacing between the toggle and the
  note above.

Tests: 3180 passed; ruff+black clean; i18n EN/JA parity. code-reviewer
APPROVE (no CRITICAL/HIGH/MEDIUM). CHANGELOG updated. Not pushed.
Two intertwined concerns (README/CHANGELOG span both; unmerged branch,
verify-after-commit checkpoint):

1) Corrects an earlier WRONG premise. A hosted HTTP MCP (Meta) IS a
   valid Claude Code user-scope server (`claude mcp add --transport
   http`, independent of / higher precedence than claude.ai
   Connectors). The 401 "✗ Failed to connect" before OAuth is the
   expected pre-auth state.
   - setup_actions._install_provider_code hosted_http: REGISTER the
     http entry (add_provider_to_claude_settings, extra_env={}), return
     new status `auth_required`. NO native auto-disable (registration ≠
     authenticated; no-strand preserved — disable only via
     `providers confirm`/native-toggle once verified Connected).
   - CLI `providers add` hosted_http: registers (not the disable
     variant) + prints accurate `/mcp` Authenticate steps (Meta no-DCR
     caveat: re-register with --client-id/--client-secret/
     --callback-port, or use the account connector). dry-run writes
     nothing.
   - auth_wizards.js: Code hosted → Install → `auth_required` → show
     `/mcp` steps; Desktop hosted unchanged (manual_required /
     Connectors). i18n connector.code.* / hosted notes / pending_meta
     rewritten (EN+JA). dashboard.js comment refreshed.
   - Claude Desktop path byte-for-byte unchanged.
   - Tests rewritten to pin the corrected behavior.

2) Docs: `mureo auth setup --web` removed → `mureo configure` is the
   documented front door. README gains a `pip install` + `mureo
   configure` quickstart; cli.md / authentication.md / getting-started
   .md(.ja) / byod.md(.ja) / architecture.md updated. CHANGELOG adds
   the corrective "Fixed — Meta hosted on Claude Code" entry (it
   explicitly supersedes the earlier Connectors-only bullets) + a
   "Docs" entry.

Tests: 3180 passed; ruff+black clean; i18n EN/JA parity; JS syntax OK.
code-reviewer APPROVE (no CRITICAL/HIGH). NOT pushed — awaiting your
real-environment /mcp verification before push/merge.
Full app.css redesign ('calm precision instrument'): refined token
system (spacing/radius/shadow scales), light + dark via
prefers-color-scheme, system fonts only (strict CSP, no web fonts),
crafted cards/buttons/pills/focus states, restrained motion +
prefers-reduced-motion. Every existing class / data-* / data-i18n
hook preserved (CSS-only, no app.html structure change beyond the
logo).

Header now shows the official mureo wordmark: bundle logo.png /
logo-dark.png into _data/web, allowlist them in handlers.py
(_STATIC_ALLOWLIST), swap by prefers-color-scheme. CSP img-src
'self' covers /static.

Test plan:
- [x] test_web_static_serving (logo.png/-dark allowlisted, served)
- [x] full suite 3182 passed; black/ruff clean; JS node -c
…register) + Google Ads scope guidance

Real-environment verification proved Meta's hosted MCP
(mcp.facebook.com/ads) has NO OAuth dynamic client registration:
Claude Code /mcp auth fails with 'redirect_uris are not registered
for this client'. Registering it as a user-scope server only
creates an unauthenticatable entry.

Claude Code path corrected: mureo no longer registers
meta-ads-official locally. install_provider / providers add return
manual_required (no ~/.claude.json write, no subprocess); the
UI/CLI steer the user to add Meta as a Claude.ai account connector
(claude.ai -> Settings -> Connectors -> Add custom connector ->
https://mcp.facebook.com/ads). No-strand preserved (mureo-native
Meta never auto-disabled; only via providers confirm once the
connector is verified Connected). Desktop unchanged. Supersedes the
earlier '/mcp register on Code' Unreleased narrative.

- setup_actions._install_provider_code hosted_http -> manual_required
- providers_cmd: _emit_hosted_auth_notice + _add_one connector copy
- auth_wizards.js: hosted (Meta) short-circuits to connector card on
  BOTH hosts; wizard.js completed reminder copy
- i18n connector.code.* + wizard.completed.pending_meta (EN/JA)
- catalog.py meta-ads-official notes; CHANGELOG/README re-corrected

Also: Google Ads OAuth scope guidance — the auth step + docs now
explain that a reused refresh token MUST carry
https://www.googleapis.com/auth/adwords or calls fail with
ACCESS_TOKEN_SCOPE_INSUFFICIENT, with the official scope-reference
URL (auth_wizards.js + i18n EN/JA + docs/authentication.md).

Test plan:
- [x] full suite 3182 passed; black/ruff clean; JS node -c; i18n parity
- [x] tests/test_cli_providers, test_web_provider_host updated to the
      not-registered/manual_required contract
- [ ] real-env: add Meta via claude.ai connector, /mcp verify
      mcp__claude_ai_MetaAds__*; re-auth Google with adwords scope
@hyoshi hyoshi merged commit 24095b8 into main May 16, 2026
8 checks passed
@hyoshi hyoshi deleted the feat/web-config-ui-phase1 branch May 16, 2026 09:06
hyoshi added a commit that referenced this pull request May 16, 2026
- pyproject 0.8.0 -> 0.9.0; Development Status 3 - Alpha -> 4 - Beta
- mureo/__init__.py __version__ 0.7.1 -> 0.9.0 (sync; was stale)
- .claude-plugin/plugin.json 0.8.0 -> 0.9.0
- CHANGELOG: [Unreleased] -> [0.9.0] - 2026-05-16, plus entries for the
  /learn restore (#105), configure UI visual refresh + official logo,
  and Google Ads OAuth-scope guidance (post-#99 merges)

Highlights: mureo configure Web UI (Phase 1) + visual refresh & logo;
mureo providers CLI; plugin->MCP tool exposure; Meta on Claude Code via
the Claude.ai connector; /learn restored; BYOD/demo date-anchor fix.

Test plan:
- [x] full suite 3209 passed
- [x] python -m build + twine check PASSED (0.9.0 wheel + sdist)
- [x] plugin manifest version-match test green
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant