jey_theme/CLAUDE.md

9.6 KiB

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/<Doctype>, /app/<slug>, 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):

{ "workspaces": { "<ws>": {
    "state": "visible|hidden|blocked",
    "cards": { "<untranslated card label>": {"state": "visible|hidden"} },
    "links": { "<Type>::<link_to>": {"state": "visible|hidden|blocked"} },  // hide/block of any link
    "added": { "<Type>::<link_to>": {"link_type","link_to","label","card"} },// injected links
    "order": { "<card label>": ["<key>", ...] }                              // 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). 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.pybefore_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 <html>, 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-<theme>-* 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 <img class="app-icon"> / .folder-icon with inline <svg class="jey-icon"> 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:<Doctype>:<field> localStorage keys and injects a <style id="jey-col-resize-styles"> with per-grid column widths, plus a MutationObserver that stamps data-jey-grid on grids as they appear.

Keep this IIFE minimal and synchronous — anything slow here delays first paint.

Adding a new theme

  1. Create jey_theme/public/css/theme_mytheme.css with [data-jey-theme="mytheme"] selectors. Prefix keyframes jey-mytheme-*.
  2. Add THEME_ICONS.mytheme = { ... } in jey_theme.js (only override keys you care about).
  3. Add an entry to JEY_THEMES array in the switcher section of jey_theme.js.
  4. Add the CSS file to both app_include_css and web_include_css in hooks.py.
  5. bench build --app jey_theme.

Rules

  • Themes can change anything (layout, positioning, colors, fonts, visibility, animations, DOM structure) — not just icons. Use JS (jeyRebuildIcons / theme-switch hooks) for DOM restructuring.
  • Never add [data-theme="dark"] or light/dark variant rules — themes are self-contained (no inherited dark/light axis).
  • Don't touch ERPNext-owned records in setup scripts; gate deletes on app == "jey_theme".