# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## What this app is Frappe/ERPNext v15 app that does three things: 1. Overrides the desk UI with a multi-theme visual system (CSS + JS bundle). 2. Hides the **Quality Management** and **Subcontracting** modules app-wide (URL/API/bootinfo/permissions). 3. Maintains a custom **Taxes Az** workspace on every migrate. Installed into `frappe-bench` as a normal app. Python code is at `jey_theme/` (Frappe app package). Frontend assets live at `jey_theme/public/{css,js}`. ## Commands Run from `frappe-bench/` (the bench root), not this app dir. - `bench build --app jey_theme` — rebuild JS bundle after editing `jey_theme/public/js/jey_theme.js`. - CSS-only changes: no build needed, just hard-reload the browser. - `bench migrate` — runs the `after_migrate` hook that rebuilds the Taxes Az workspace from `setup/taxes_workspace.py`. - `bench execute jey_theme.setup.taxes_workspace.setup` — re-run the workspace setup without a full migrate. - `bench restart` — reload Python workers after editing `access_control.py` or `hooks.py`. - No test suite, no linter config in this repo. ## Architecture ### Python: access control (`jey_theme/access_control.py` + `hooks.py`) Goal: make blocked modules invisible and unreachable for every user, including Administrator. - `BLOCKED_MODULES` / `BLOCKED_DOCTYPES_EXTRA` — edit these frozensets to change what's blocked. - Three defense layers wired in `hooks.py`: - `before_request` — blocks `/api/resource/`, `/app/`, and form_dict-based calls at the HTTP boundary (catches Administrator, who bypasses `has_permission`). - `has_permission = {"*": ...}` — per-doc deny for non-admin users who have role access. - `extend_bootinfo` — strips blocked items from `allowed_modules`, `modules`, `user.can_*`, `workspaces.pages`, `workspace_sidebar_item`, `module_wise_workspaces`, `app_data`, `desktop_icons`, etc., so the client router/sidebar never sees them. - Slug/doctype/workspace sets are cached on `frappe.local` per-request. - When adding a new block vector: extend `extend_bootinfo` rather than relying only on `has_permission`, because the client has many independent boot-derived UIs. ### Python: desk layout engine (`jey_theme/desk_layout.py` + `desk_overrides.py`) Configurable, **non-destructive** (variant A) control of desk workspaces/cards/links, driven by a JSON config. Lets a System Manager, inline on `/desk`, **hide**, **block**, **add** and **reorder** links. The companion editor is the separate `jey_layout` app (install on the design machine only); the engine here is enforced on every site. The Workspace DB records are never modified — fully reversible, survives ERPNext migrations (everything is applied at request time, server-side → no client flash). **Config** (`jey_theme/config/desk_layout.json` = shipped snapshot; per machine the editor writes a working copy in `jey_layout/config/desk_layout.json`): ```jsonc { "workspaces": { "": { "state": "visible|hidden|blocked", "cards": { "": {"state": "visible|hidden"} }, "links": { "::": {"state": "visible|hidden|blocked"} }, // hide/block of any link "added": { "::": {"link_type","link_to","label","card"} },// injected links "order": { "": ["", ...] } // per-card link order } } } ``` Key for every link = `link_type::link_to` (Type ∈ DocType/Report/Page). - **hide** = the item is filtered OUT of the data the server sends (sidebar / `get_desktop_page` cards+links) → absent in the DOM. URL still works. - **block** = ALSO added to `blocked_doctypes/reports/pages` → denied at `before_request` (HTTP/URL/API, catches Administrator) + `has_permission` (per-doc). Truly unreachable for everyone. - **add / reorder** = structural overlay: `get_desktop_page` injects added links into their card and reorders per `order`. Applied ALWAYS (even in edit mode); only hide/block is gated by enforcement. **Files** - `desk_layout.py` — loads config; `_cache()`/`_build()` (enforcing-gated, fail-open) produce hide/block sets cached on `frappe.local`; `_raw_workspaces()` + `added_links()`/`order_map()`/`apply_order()` are the structural (always-on) overlay. `is_enforcing()`: production → always; design machine → `not is_editing()` (Redis key `jey_layout_editing`). `is_design_mode()` defaults to ON exactly when the `jey_layout` app is installed (test machines); `jey_design_mode` in site_config overrides (0/1). Tolerates missing refs. - `desk_overrides.py` — wraps `get_workspace_sidebar_items`/`get_desktop_page` via `override_whitelisted_methods`. `get_desktop_page` does inject → reorder (always) → hide/block filter (only when enforcing). `_build_link_item` shapes an injected link like frappe's so the client renders it natively. Card labels in output are translated, so matching forward-translates config labels (`_(label)`). - `access_control.py` — `before_request` + `has_permission` deny `blocked` doctypes/reports/pages (alongside static `BLOCKED_MODULES`); `extend_bootinfo` strips hidden/blocked workspaces from boot and sets `bootinfo.jey_design_mode`. All three are wrapped fail-open (never break the desk; errors → Error Log). **Edit vs applied model** (no "preview"): on the design machine, the normal desk is the *applied* result; "✎ Edit layout" flips `jey_layout_editing` and reloads to show all items + controls; "✓ Done editing" reloads back to applied. Export copies the working file into this snapshot for shipping. > ⚠️ **Edge gotcha — DO NOT load the editor via `app_include`.** `app_include_js/css` > assets are added to the HTTP `Link: rel=preload` response header. On this > deployment the desk's total response headers are ~4 KB and the edge proxy's > `proxy_buffer_size` is 4 KB; two extra app_include entries pushed it over → > `502 "upstream sent too big header"` (local nginx has a bigger buffer, so it only > failed through the edge). Instead `jey_theme.js` loads `jey_layout`'s JS/CSS > **dynamically** (design mode only, via `bootinfo.jey_design_mode`), adding nothing > to the header. Proper infra fix (not required by the app): raise the edge's > `proxy_buffer_size`/`large_client_header_buffers`. ### Python: Taxes Az workspace (`jey_theme/setup/taxes_workspace.py`) Idempotent setup run via `after_migrate`. - Builds a `Workspace` named **Taxes Az** with six cards (Declarations, Setup, Tools, E-Taxes Setup, E-Taxes Reference, E-Taxes Documents), a `Workspace Sidebar` listing every doctype, and a `Desktop Icon` that routes via the sidebar. - The doctype list is a **static snapshot** in `DECLARATIONS_DOCTYPES` / `SETUP_DOCTYPES` / `TOOLS_DOCTYPES` (taxes_az) and `ETAXES_SETUP_DOCTYPES` / `ETAXES_REFERENCE_DOCTYPES` / `ETAXES_DOCUMENT_DOCTYPES` (invoice_az). Adding a DocType to either app does NOT auto-include it — append to the tuple and re-migrate. - `_cleanup_obsolete()` removes legacy docs owned by this app (currently `"Taxes"`). Only touches records where `app == "jey_theme"` — never deletes ERPNext/other-app docs. ### Frontend: multi-theme system Bundle entry: `jey_theme/public/js/jey_theme.js`. CSS: `jey_theme/public/css/{shared,theme_chrome,theme_modern}.css`. All four are loaded via `app_include_css` / `app_include_js` in `hooks.py` for both desk and web contexts. - Theme selector lives in `data-jey-theme` on ``, stored in `localStorage("jey-theme")`. Default: `chrome`. Set before first paint by an IIFE at the top of `jey_theme.js`. - Themes supported: **chrome** (metallic rings), **modern** (frosted glass). No dark/light variants — each theme is self-contained. - Theme-specific CSS MUST be wrapped in `[data-jey-theme="themename"]`. Keyframes MUST be prefixed `jey--*` to avoid cross-theme name collisions. - Shared CSS (checkboxes, icon base sizing, switcher UI, column-resize rules) lives in `shared.css` without theme selectors. ### Frontend: per-theme icons - `THEME_ICONS[themeName]` in `jey_theme.js` maps icon keys to inline SVG path strings. Missing keys fall back to `THEME_ICONS.chrome`, then `_fallback`. - JS replaces `` / `.folder-icon` with inline `` per current theme. - `window.jeyRebuildIcons()` tears down and rebuilds all icon DOM — called on theme switch. - SVGs may carry a `cls` (e.g. `ico-buying`) for shared CSS animations. ### Frontend: early-paint tricks in `jey_theme.js` The top IIFE runs before paint to avoid FOUC: - Sets `data-jey-theme` immediately. - Defaults `container_fullwidth = "true"` on fresh install. - Reads all `jey-col-widths::` localStorage keys and injects a `