167 lines
11 KiB
Markdown
167 lines
11 KiB
Markdown
# 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`):
|
|
```jsonc
|
|
{ "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 denied at the access layer, gated to PRODUCTION only (where
|
|
`jey_layout` is absent / `jey_design_mode` is 0; the design machine keeps access so
|
|
admins can manage). Two enforcement paths:
|
|
- **doctypes** → `permission_query_conditions` (`*` → returns `1=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 from `can_*`). NOT
|
|
`before_request`: a blunt URL/API block on a doctype breaks `search_link`/dynamic
|
|
links, whereas permission hooks are bypassed by `ignore_permissions` so 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.
|
|
- **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()` parse it ONCE per request
|
|
(ungated, fail-open) into hide/block sets on `frappe.local`. Two accessor families
|
|
with DIFFERENT gates: VISUAL overlay (`should_hide_*`, `hidden_or_blocked_workspaces`)
|
|
is gated on `is_enforcing()`; ACCESS-block (`prod_blocked_doctypes`/`_reports`/
|
|
`_pages`/`_workspaces`) is gated on `not 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 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` — enforcement. Static `BLOCKED_MODULES` (Quality/Subcontracting)
|
|
+ config reports/pages/workspaces are denied at `before_request` (HTTP, catches
|
|
Administrator) and `has_permission`. Config-blocked **doctypes** are denied via
|
|
`permission_query_conditions` (`*` → `1=0`) + `has_permission` (per-doc) ONLY — not
|
|
`before_request` (keeps `search_link`/dynamic links working). `extend_bootinfo`
|
|
strips blocked modules/doctypes (`can_*`) and hidden/blocked workspaces from boot,
|
|
and sets `bootinfo.jey_design_mode`. All hooks are wrapped fail-open (never break the
|
|
desk; `has_permission` returns True on error, `before_request` re-raises only the
|
|
intended `PermissionError`; 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), **silk** (warm ivory & champagne-gold, frosted cards). 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"`.
|