13 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— required after CSS edits (jey_theme.bundle.css→ hashed file inpublic/dist/css/). JS is served raw and needs only a hard reload.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 denied at the access layer, gated to PRODUCTION only (where
jey_layoutis absent /jey_design_modeis 0; the design machine keeps access so admins can manage). Two enforcement paths:- doctypes →
permission_query_conditions(*→ returns1=0, so the list is EMPTY for everyone incl. System Manager) +has_permission(per-doc deny, every ptype: read/create/write/delete) +extend_bootinfo(stripped fromcan_*). NOTbefore_request: a blunt URL/API block on a doctype breakssearch_link/dynamic links, whereas permission hooks are bypassed byignore_permissionsso those internal reads keep working. Code-enforced ⇒ SM-proof (lives in code+config, not DocPerm, so a System Manager can't undo it from the UI). Administrator is the only break-glass bypass. - reports / pages / workspaces →
before_request(HTTP/URL/API, catches Administrator too) +has_permission(per-doc). Truly unreachable for everyone.
- doctypes →
- 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()parse it ONCE per request (ungated, fail-open) into hide/block sets onfrappe.local. Two accessor families with DIFFERENT gates: VISUAL overlay (should_hide_*,hidden_or_blocked_workspaces) is gated onis_enforcing(); ACCESS-block (prod_blocked_doctypes/_reports/_pages/_workspaces) is gated onnot is_design_mode()(block bites on prod only)._ws_entry()+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— enforcement. StaticBLOCKED_MODULES(Quality/Subcontracting)- config reports/pages/workspaces are denied at
before_request(HTTP, catches Administrator) andhas_permission. Config-blocked doctypes are denied viapermission_query_conditions(*→1=0) +has_permission(per-doc) ONLY — notbefore_request(keepssearch_link/dynamic links working).extend_bootinfostrips blocked modules/doctypes (can_*) and hidden/blocked workspaces from boot, and setsbootinfo.jey_design_mode. All hooks are wrapped fail-open (never break the desk;has_permissionreturns True on error,before_requestre-raises only the intendedPermissionError; errors → Error Log).
- config reports/pages/workspaces are denied at
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
JS entry: jey_theme/public/js/jey_theme.js (served raw via app_include_js, not bundled).
CSS: one file, jey_theme/public/css/jey_theme.bundle.css — the former
shared.css / theme_chrome.css / theme_modern.css were concatenated into it
(in that order, still marked by /* ===== <name>.css ===== */ banners) so the
desk's preload header stays under the edge proxy's 4 KB buffer. Edit the bundle
directly; there are no separate source files to regenerate it from.
- CSS changes need
bench build --app jey_theme.app_include_cssnames the bundle, and esbuild emits the hashedpublic/dist/css/jey_theme.bundle.<HASH>.cssthatsites/assets/assets.jsonactually points the browser at. Editing the bundle alone changes nothing in the browser. - JS changes need only a hard reload — but
/assets/jey_theme/js/jey_theme.jsis a hashless path under a 1-year nginx cache, so it really does have to be a hard reload (or a cache-disabled session). - 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), silk (warm ivory & champagne-gold
glass-neumorphism, ported from
public/silk_dashboard_crisp.html— that mockup is silk's design reference; keep the two in sync). 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, the
card icon-tile defaults) lives in the
shared.csssection without theme selectors.
Frosted glass (silk, and any future glass theme) — two rules that cost real debugging time:
- One
backdrop-filterper stack. A frosted element inside another frosted element paints as an opaque white block on real GPUs; headless Chrome (SwiftShader) renders it correctly, so it passes every test render. Only the outermost surface gets the filter — currently.widget,.form-page,.frappe-list .result-container,.navbar-search-bar,.dropdown-navbar-user,.desktop-navbar-modal-search,.jey-chrome. Everything nested (icon tiles, chips, count pills, buttons) gets a translucent fill and no filter. There's a backstop rule near the Modals section, but don't rely on it — check with the live-DOM audit (walk every element, flag any whose ancestor also has a non-nonecomputedbackdropFilter). - On silk it's the translucency, not the blur, that reads as glass. The fabric is near-uniform, so a strong blur averages it into a flat opaque panel. Light blur (6px) over a very translucent warm fill is what lets it show through.
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
- Append a
/* ===== theme_mytheme.css ===== */section tojey_theme/public/css/jey_theme.bundle.css, with every rule wrapped in[data-jey-theme="mytheme"]. Prefix keyframesjey-mytheme-*. Do NOT add a new file/app_include_cssentry — extra preload-header lines are what the single bundle exists to avoid. - Add
THEME_ICONS.mytheme = { ... }injey_theme.js(only override keys you care about). - Add an entry to the
JEY_THEMESarray in the switcher section ofjey_theme.js, and a.jey-theme-preview--mythemeswatch in the bundle's shared section. 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".