9.7 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:
- Overrides the desk UI with a multi-theme visual system (CSS + JS bundle).
- Hides the Quality Management and Subcontracting modules app-wide (URL/API/bootinfo/permissions).
- 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 editingjey_theme/public/js/jey_theme.js.- CSS-only changes: no build needed, just hard-reload the browser.
bench migrate— runs theafter_migratehook that rebuilds the Taxes Az workspace fromsetup/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 editingaccess_control.pyorhooks.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 bypasseshas_permission).has_permission = {"*": ...}— per-doc deny for non-admin users who have role access.extend_bootinfo— strips blocked items fromallowed_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.localper-request. - When adding a new block vector: extend
extend_bootinforather than relying only onhas_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_pagecards+links) → absent in the DOM. URL still works. - block = ALSO added to
blocked_doctypes/reports/pages→ denied atbefore_request(HTTP/URL/API, catches Administrator) +has_permission(per-doc). Truly unreachable for everyone. - add / reorder = structural overlay:
get_desktop_pageinjects added links into their card and reorders perorder. 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 onfrappe.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 keyjey_layout_editing).is_design_mode()defaults to ON exactly when thejey_layoutapp is installed (test machines);jey_design_modein site_config overrides (0/1). Tolerates missing refs.desk_overrides.py— wrapsget_workspace_sidebar_items/get_desktop_pageviaoverride_whitelisted_methods.get_desktop_pagedoes inject → reorder (always) → hide/block filter (only when enforcing)._build_link_itemshapes 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_permissiondenyblockeddoctypes/reports/pages (alongside staticBLOCKED_MODULES);extend_bootinfostrips hidden/blocked workspaces from boot and setsbootinfo.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/cssassets are added to the HTTPLink: rel=preloadresponse header. On this deployment the desk's total response headers are ~4 KB and the edge proxy'sproxy_buffer_sizeis 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). Insteadjey_theme.jsloadsjey_layout's JS/CSS dynamically (design mode only, viabootinfo.jey_design_mode), adding nothing to the header. Proper infra fix (not required by the app): raise the edge'sproxy_buffer_size/large_client_header_buffers.
Python: Taxes Az workspace (jey_theme/setup/taxes_workspace.py)
Idempotent setup run via after_migrate.
- Builds a
Workspacenamed Taxes Az with six cards (Declarations, Setup, Tools, E-Taxes Setup, E-Taxes Reference, E-Taxes Documents), aWorkspace Sidebarlisting every doctype, and aDesktop Iconthat routes via the sidebar. - The doctype list is a static snapshot in
DECLARATIONS_DOCTYPES/SETUP_DOCTYPES/TOOLS_DOCTYPES(taxes_az) andETAXES_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 whereapp == "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-themeon<html>, stored inlocalStorage("jey-theme"). Default:chrome. Set before first paint by an IIFE at the top ofjey_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 prefixedjey-<theme>-*to avoid cross-theme name collisions. - Shared CSS (checkboxes, icon base sizing, switcher UI, column-resize rules) lives in
shared.csswithout theme selectors.
Frontend: per-theme icons
THEME_ICONS[themeName]injey_theme.jsmaps icon keys to inline SVG path strings. Missing keys fall back toTHEME_ICONS.chrome, then_fallback.- JS replaces
<img class="app-icon">/.folder-iconwith 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-themeimmediately. - 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 stampsdata-jey-gridon grids as they appear.
Keep this IIFE minimal and synchronous — anything slow here delays first paint.
Adding a new theme
- Create
jey_theme/public/css/theme_mytheme.csswith[data-jey-theme="mytheme"]selectors. Prefix keyframesjey-mytheme-*. - Add
THEME_ICONS.mytheme = { ... }injey_theme.js(only override keys you care about). - Add an entry to
JEY_THEMESarray in the switcher section ofjey_theme.js. - Add the CSS file to both
app_include_cssandweb_include_cssinhooks.py. 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".