diff --git a/create_pod_espocrm.sh b/create_pod_espocrm.sh index ea68468..6ce4839 100755 --- a/create_pod_espocrm.sh +++ b/create_pod_espocrm.sh @@ -25,6 +25,13 @@ BIND_DIR="$HOME/.local/share/$POD_NAME" DB_DATA_DIR="$BIND_DIR/mariadb" # -> /var/lib/mysql ESPO_DATA_DIR="$BIND_DIR/espocrm" # -> /var/www/html +# GUI customizations snapshot, version-controlled in this repo next to the script. +# Deployed into /var/www/html on every provisioning run so a reset+reprovision +# restores all customizations (CTag entity, layouts, i18n, clientDefs, custom +# views, custom CSS, ...). Refresh it after GUI changes with snapshot_espocrm_custom.sh. +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +CUSTOM_SRC="$SCRIPT_DIR/espocrm-custom" # contains custom/ and client/custom/ + # systemd --user paths USER_SYSTEMD_DIR="$HOME/.config/systemd/user" WEB_UNIT_FILE="$USER_SYSTEMD_DIR/container-${WEB_CTR}.service" @@ -103,50 +110,22 @@ for i in $(seq 1 90); do done echo "EspoCRM responded with HTTP $code (rc=$?)" -# --- GUI customization: variable column widths for Markdown tables ------------- -# Core CSS forces `table-layout: fixed; width: 100%` on `.complex-text table`, -# giving every column an equal width. Register a custom CSS file via cssList so -# those tables size columns to content (table-layout: auto). Created by this -# provisioning script (on the persisted bind-mount) so it survives re-runs; the -# rebuild step below refreshes the cached cssList metadata. -podman exec "$WEB_CTR" sh -lc 'mkdir -p /var/www/html/client/custom/css /var/www/html/custom/Espo/Custom/Resources/metadata/app' -podman exec -i "$WEB_CTR" sh -c 'cat > /var/www/html/client/custom/css/table-var-col.css' <<'CSSEOF' -/** - * Variable column widths for Markdown tables in text fields. - * - * EspoCRM core CSS forces `table-layout: fixed; width: 100%` on - * `.complex-text table`, which makes all columns equally wide. - * `auto` lets columns size to content instead. - * - * Registered via custom/Espo/Custom/Resources/metadata/app/client.json (cssList). - */ -.complex-text table { - table-layout: auto !important; - width: auto !important; - max-width: 100% !important; -} -CSSEOF -echo "Wrote client/custom/css/table-var-col.css (rc=$?)" - -# Merge the CSS path into cssList (idempotent; preserves other keys/entries and -# keeps "__APPEND__" first). Uses the container's php so no host jq is required. -podman exec "$WEB_CTR" php -r ' - $file = "/var/www/html/custom/Espo/Custom/Resources/metadata/app/client.json"; - $css = "client/custom/css/table-var-col.css"; - $data = is_file($file) ? json_decode(file_get_contents($file), true) : []; - if (!is_array($data)) { $data = []; } - $list = (isset($data["cssList"]) && is_array($data["cssList"])) ? $data["cssList"] : []; - $list = array_values(array_filter($list, function ($x) use ($css) { return $x !== "__APPEND__" && $x !== $css; })); - array_unshift($list, "__APPEND__"); - $list[] = $css; - $data["cssList"] = $list; - file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"); -' -echo "Registered table-var-col.css in cssList (rc=$?)" - -# Match ownership of the other EspoCRM customization files (webserver user). -podman exec "$WEB_CTR" chown -R www-data:www-data /var/www/html/client/custom /var/www/html/custom/Espo/Custom/Resources/metadata -echo "Fixed ownership of customizations (rc=$?)" +# --- Deploy all GUI customizations from the repo snapshot --------------------- +# Copies the version-controlled custom/ and client/custom/ trees into the pod so +# a reset+reprovision restores everything: the CTag entity (controller, scopes, +# recordDefs, i18n), layouts, clientDefs (Tags +button removed, row-actions = +# Unlink only), the signature image-button view, and the table-var-col CSS. +# Extract as container-root (--no-same-owner), then chown to the webserver user. +# The rebuild step below refreshes the cached metadata (entityDefs, cssList, ...). +if [ -d "$CUSTOM_SRC" ]; then + tar -C "$CUSTOM_SRC" -cf - custom client/custom \ + | podman exec -i "$WEB_CTR" tar -C /var/www/html -xf - --no-same-owner + echo "Deployed GUI customizations from $CUSTOM_SRC (rc=$?)" + podman exec "$WEB_CTR" chown -R www-data:www-data /var/www/html/custom /var/www/html/client/custom + echo "Fixed ownership of customizations (rc=$?)" +else + echo "WARNING: customization snapshot not found at $CUSTOM_SRC; skipping deploy." +fi # Rebuild EspoCRM cache so customizations on the persisted bind-mount take effect # and the client cacheTimestamp is bumped (forces browsers to reload custom views, diff --git a/espoImage-button-in-signature-editor.md b/espoImage-button-in-signature-editor.md new file mode 100644 index 0000000..37186ea --- /dev/null +++ b/espoImage-button-in-signature-editor.md @@ -0,0 +1,105 @@ +### Goal + +Restore the image button (`espoImage`) in the EspoCRM **signature** editor toolbar. The signature field uses the core view `views/preferences/fields/signature`, whose `setupToolbar()` overrides the toolbar to `[["style",[…]],["color",["color"]],["height",["height"]],["table",["espoLink"]],["misc",[,"fullscreen"]]]` — i.e. it drops the `espoImage` button that the generic wysiwyg field has. We want the image button back **only for the signature field**, without changing anything else. + +Do **not** touch other fields, ACLs, the generic wysiwyg editor (email body / templates already have the image button), or any data. + +### Environment + +- EspoCRM **9.3.8** (Apache variant) runs as a **rootless Podman pod** `espocrm_pod` on this server. Web container is expected to be `espocrm_ctr` (confirm with `podman ps`). +- App root inside the web container: `/var/www/html`, runs as user **`www-data`**. +- Customizations live on the persisted `/var/www/html` bind-mount, same locations as the S47 task: + - Backend metadata: `/var/www/html/custom/Espo/Custom/Resources/metadata/…` + - Frontend custom views: `/var/www/html/client/custom/src/…` (referenced with the `custom:` prefix). + +### Mechanism (from 9.3.8 source — confirm against this instance before relying on it) + +- The signature field view `client/src/views/preferences/fields/signature.js` extends the wysiwyg field and **overrides** `setupToolbar()`. Its `this.toolbar` contains a group `['table', ['espoLink']]` — the link button but **not** `espoImage`. +- The generic wysiwyg field `client/src/views/fields/wysiwyg.js` registers the upload callback in `enableWysiwygMode()`: `onImageUpload` → `uploadInlineAttachment(file)` → creates an `Attachment` (`role: 'Inline Attachment'`) and inserts ``. This callback is present regardless of the toolbar, so as soon as the `espoImage` button is in the toolbar, its upload path produces an attachment. +- `espoImage` is a registered summernote plugin (`client/src/helpers/misc/summernote-custom.js`): its button invokes `espoImage.show` → modal `views/wysiwyg/modals/insert-image`, whose **upload** path runs through `insertImagesOrCallback` → the `onImageUpload` callback above. (The modal's **URL** path inserts a plain remote `` instead — not what we want.) + +Verify before relying on it: + +``` +podman exec espocrm_ctr sh -lc "grep -n 'setupToolbar\|espoLink\|espoImage\|table' /var/www/html/client/src/views/preferences/fields/signature.js" +podman exec espocrm_ctr sh -lc "grep -n 'onImageUpload\|uploadInlineAttachment\|entryPoint=attachment' /var/www/html/client/src/views/fields/wysiwyg.js" +``` + +You should see the reduced toolbar with `['table', ['espoLink']]` and the `onImageUpload`→attachment path. If the structure differs in this build, adjust the toolbar-patch below to match the real group names. + +### Steps + +1. **Confirm container.** `podman ps` → identify the web container (expected `espocrm_ctr`). + +2. **Create the custom signature field view** on the persisted mount: + + `/var/www/html/client/custom/src/views/preferences/fields/signature.js` + + ```js + define('custom:views/preferences/fields/signature', + ['views/preferences/fields/signature'], (SignatureFieldView) => { + + return class extends SignatureFieldView { + + setupToolbar() { + // Build the reduced signature toolbar first … + super.setupToolbar(); + + // … then re-add the image button next to the link button. + this.toolbar = (this.toolbar || []).map(group => { + if (Array.isArray(group) && group[0] === 'table') { + const buttons = group[1] || []; + + if (!buttons.includes('espoImage')) { + return ['table', [...buttons, 'espoImage']]; + } + } + + return group; + }); + } + }; + }); + ``` + + Create the directory if needed: + ``` + podman exec espocrm_ctr sh -lc "mkdir -p /var/www/html/client/custom/src/views/preferences/fields" + ``` + + > If the `grep` in the verify step shows no group whose first element is `'table'`, fall back to simply appending a new group instead of the `.map(...)`: `this.toolbar.push(['insert', ['espoImage']]);` (place it before the final `['misc', …]` group if you want it left of code-view/fullscreen). + +3. **Point the signature field at the custom view** via an entityDefs override. Merge into (create if absent): + + `/var/www/html/custom/Espo/Custom/Resources/metadata/entityDefs/Preferences.json` + + ```json + { + "fields": { + "signature": { + "view": "custom:views/preferences/fields/signature" + } + } + } + ``` + + **MERGE** — if the file already exists, preserve all other keys and only add `fields.signature.view`. Do not drop the field's `type` (it stays `wysiwyg`; metadata is merged on top of core). Produce valid JSON. + +4. **Fix ownership** (as root in the container): + ``` + podman exec espocrm_ctr chown -R www-data:www-data /var/www/html/client/custom /var/www/html/custom/Espo/Custom/Resources/metadata + ``` + +5. **Clear cache / rebuild** (as the web user): + ``` + podman exec -u www-data espocrm_ctr php command.php rebuild + ``` + (If `command.php` is missing in this build, use `bin/command rebuild`; or Administration → Rebuild in the UI as admin.) + +6. **Verify (hard reload Ctrl+Shift+R first).** Open Benutzereinstellungen → the „E-Mail Signatur" editor. The toolbar must now show a **Bild/Image button** (picture icon) next to the link button. Click it → a „Bild einfügen" dialog opens with a **file-upload** option (and a URL field). Nothing else in EspoCRM should have changed. + +### Rollback + +1. Delete `/var/www/html/client/custom/src/views/preferences/fields/signature.js`. +2. Remove the `fields.signature.view` key from `…/metadata/entityDefs/Preferences.json` (or delete the file if it now only contains that key). +3. Rebuild (step 5) and hard-reload the browser. diff --git a/espocrm-custom/client/custom/css/table-var-col.css b/espocrm-custom/client/custom/css/table-var-col.css new file mode 100644 index 0000000..60763b7 --- /dev/null +++ b/espocrm-custom/client/custom/css/table-var-col.css @@ -0,0 +1,14 @@ +/** + * Variable column widths for Markdown tables in text fields. + * + * EspoCRM core CSS forces `table-layout: fixed; width: 100%` on + * `.complex-text table`, which makes all columns equally wide. + * `auto` lets columns size to content instead. + * + * Registered via custom/Espo/Custom/Resources/metadata/app/client.json (cssList). + */ +.complex-text table { + table-layout: auto !important; + width: auto !important; + max-width: 100% !important; +} diff --git a/espocrm-custom/client/custom/modules/dummy.txt b/espocrm-custom/client/custom/modules/dummy.txt new file mode 100644 index 0000000..5a1126a --- /dev/null +++ b/espocrm-custom/client/custom/modules/dummy.txt @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/espocrm-custom/client/custom/src/views/preferences/fields/signature.js b/espocrm-custom/client/custom/src/views/preferences/fields/signature.js new file mode 100644 index 0000000..d498e47 --- /dev/null +++ b/espocrm-custom/client/custom/src/views/preferences/fields/signature.js @@ -0,0 +1,24 @@ +define('custom:views/preferences/fields/signature', +['views/preferences/fields/signature'], (SignatureFieldView) => { + + return class extends SignatureFieldView { + + setupToolbar() { + // Build the reduced signature toolbar first … + super.setupToolbar(); + + // … then re-add the image button next to the link button. + this.toolbar = (this.toolbar || []).map(group => { + if (Array.isArray(group) && group[0] === 'table') { + const buttons = group[1] || []; + + if (!buttons.includes('espoImage')) { + return ['table', [...buttons, 'espoImage']]; + } + } + + return group; + }); + } + }; +}); diff --git a/espocrm-custom/client/custom/src/views/record/row-actions/tags-unlink-only.js b/espocrm-custom/client/custom/src/views/record/row-actions/tags-unlink-only.js new file mode 100644 index 0000000..0881e09 --- /dev/null +++ b/espocrm-custom/client/custom/src/views/record/row-actions/tags-unlink-only.js @@ -0,0 +1,28 @@ +define('custom:views/record/row-actions/tags-unlink-only', +['views/record/row-actions/relationship'], (RelationshipRowActionsView) => { + + return class extends RelationshipRowActionsView { + + getActionList() { + const list = []; + + // Keep only the "Unlink" action (German UI: "Link entfernen"). + // View / Edit / Remove are intentionally omitted. + if (!this.options.unlinkDisabled) { + list.push({ + action: 'unlinkRelated', + label: 'Unlink', + data: { + id: this.model.id, + }, + groupIndex: 0, + }); + } + + // Preserve any explicitly configured extra row actions (none by default). + this.getAdditionalActionList().forEach(item => list.push(item)); + + return list; + } + }; +}); diff --git a/espocrm-custom/custom/Espo/Custom/.htaccess b/espocrm-custom/custom/Espo/Custom/.htaccess new file mode 100644 index 0000000..09acdfd --- /dev/null +++ b/espocrm-custom/custom/Espo/Custom/.htaccess @@ -0,0 +1,2 @@ +Order Deny,Allow +Deny from all \ No newline at end of file diff --git a/espocrm-custom/custom/Espo/Custom/Controllers/CTag.php b/espocrm-custom/custom/Espo/Custom/Controllers/CTag.php new file mode 100644 index 0000000..48c83eb --- /dev/null +++ b/espocrm-custom/custom/Espo/Custom/Controllers/CTag.php @@ -0,0 +1,7 @@ + .message table, .complex-text table, +.confirm-message table, .popover-content table { + table-layout: fixed; + width: 100%; +} +``` + +`table-layout: fixed` distributes the table width equally across all columns regardless of content. Measured effect on a 4-column table in a record detail view: every column 200 px wide, causing short columns (`Nr.`, `Bew.`) to waste space and the long `Anforderung` column to wrap into unnecessarily tall rows. + +With `table-layout: auto`, columns size to their content. Verified by live style injection on the test record: column widths became 37 / 112 / 49 / 604 px — exactly the desired result. + +## Solution: EspoCRM Custom CSS Mechanism + +EspoCRM's official mechanism for custom stylesheets ([docs](https://docs.espocrm.com/development/custom-css/)): register the CSS file in the `cssList` metadata. Both involved directories (`client/custom/`, `custom/`) are upgrade-safe — EspoCRM upgrades do not touch them. The CSS filename is arbitrary; we use the declarative name `table-var-col.css`. + +### File 1 (new): `/client/custom/css/table-var-col.css` + +```css +/** + * Variable column widths for Markdown tables in text fields. + * + * EspoCRM core CSS forces `table-layout: fixed; width: 100%` on + * `.complex-text table`, which makes all columns equally wide. + * `auto` lets columns size to content instead. + * + * Registered via custom/Espo/Custom/Resources/metadata/app/client.json (cssList). + */ +.complex-text table { + table-layout: auto !important; + width: auto !important; + max-width: 100% !important; +} +``` + +Notes: + +- The selector deliberately targets only `.complex-text` (text fields on record views), not the alert/popover tables of the core rule. +- `!important` guards against load-order variations; `cssList` entries are appended after the theme stylesheet, but this makes the override robust either way. + +### File 2 (new or merge): `/custom/Espo/Custom/Resources/metadata/app/client.json` + +```json +{ + "cssList": [ + "__APPEND__", + "client/custom/css/table-var-col.css" + ] +} +``` + +**IMPORTANT — merge, do not overwrite:** This instance already has GUI customizations. If `client.json` already exists, append the path to the existing `cssList` array (keeping `"__APPEND__"` as its first element) and leave all other keys untouched. Check first. + +## Integration into the Provisioning Script + +1. Locate where the script already injects EspoCRM GUI customizations and follow the same convention (e.g. `podman cp`, volume mount, or heredoc). +2. Create both files after the EspoCRM installation step. +3. Match file ownership/permissions to the other EspoCRM application files (typically the webserver user inside the container, e.g. `www-data:www-data`). The CSS file must be readable by the webserver. +4. Clear the cache afterwards — `cssList` is metadata and is cached: + +```bash +podman exec php /var/www/html/clear_cache.php +# or, if the script already performs a rebuild: +podman exec php /var/www/html/rebuild.php +``` + +(Adapt container name and EspoCRM root path to the script's actual values.) + +## Testing + +1. **Persistence test (the actual requirement):** re-run the provisioning script from scratch, then verify both files exist inside the pod: + `podman exec ls -l /var/www/html/client/custom/css/table-var-col.css /var/www/html/custom/Espo/Custom/Resources/metadata/app/client.json` +2. Log in to the CRM, hard-reload (`Ctrl+F5`). In DevTools → Network, filter for `table-var-col` → the stylesheet must load with HTTP 200. +3. Open the test record (4-column Markdown table in `Beschreibung`): + `https://crm.creature-go.com/#Opportunity/view/6a23fc99e2391e7be` +4. Visual check: columns `Nr.`, `Einordnung`, `Bew.` at minimal width, `Anforderung` takes the remaining space; rows are one to two lines instead of uniformly tall. +5. Programmatic check in the DevTools console: + +```js +getComputedStyle(document.querySelector('.complex-text table')).tableLayout +// expected: "auto" (without the fix: "fixed") +``` + +6. Regression check: open a record whose description contains no table and one with other Markdown (lists, headings) — rendering must be unchanged. Existing GUI customizations must still be active. diff --git a/espocrm-tags-rowactions-unlink-only-claude-code.md b/espocrm-tags-rowactions-unlink-only-claude-code.md new file mode 100644 index 0000000..d5b0084 --- /dev/null +++ b/espocrm-tags-rowactions-unlink-only-claude-code.md @@ -0,0 +1,154 @@ +# Task: Reduce the Tags relationship-panel row menu to "Unlink" only in EspoCRM + +## Goal + +This EspoCRM instance has a custom many-to-many relationship to a custom `Tag` entity, exposed as a **"Tags" bottom relationship panel** on three entity types: **Opportunity**, **Contact**, and **Account**. On each of these three entities the link is named **`cTags`**. (The inline **Create (+)** button on these panels was already removed in a previous task via `relationshipPanels.cTags.create = false` — leave that in place.) + +On a record's Tags panel, each attached tag row has a small dropdown (triangle/▾) that currently offers **four** actions: **View**, **Edit**, **Unlink**, **Remove** (German UI: **Ansehen**, **Bearbeiten**, **Link entfernen**, **Löschen**). + +We want each tag row's dropdown to offer **only "Unlink"** (German: **"Link entfernen"**). The actions **View**, **Edit**, and **Remove/Delete** must be gone. This must apply to **all** users, including regular (non-admin) users, on **all three** entities (Opportunity, Contact, Account). + +Do **not** change anything else (no ACLs, no other panels/fields/entities, and do not touch the `Tag` entity itself — creating, viewing, editing and deleting tags from the **Tag** entity's own list/detail views must keep working normally). + +## Environment + +- EspoCRM (version **9.3.8**, Apache variant) runs as a **rootless Podman pod** named `espocrm_pod` on this server. The web container is expected to be `espocrm_ctr` (confirm with `podman ps`). +- EspoCRM application root inside the web container: `/var/www/html`. The app runs as user **`www-data`**. +- Customizations live under `/var/www/html/` and must be on the **persisted** bind-mount so they survive container recreation. Two locations are used here: + - Backend metadata: `/var/www/html/custom/Espo/Custom/Resources/metadata/clientDefs/` (this already holds `Opportunity.json`, `Contact.json`, `Account.json` from the +button task). + - Frontend custom view: `/var/www/html/client/custom/src/` (this is the official location for custom client-side views; it is part of the same persisted `/var/www/html` that already keeps your `custom/` metadata across restarts). + +## Mechanism (verified against EspoCRM 9.3.8 source) + +The per-row dropdown of a relationship panel is rendered by the view **`views/record/row-actions/relationship`**. Its `getActionList()` builds the menu like this (paraphrased from `client/src/views/record/row-actions/relationship.js`): + +- **View** — pushed **unconditionally** (always present; there is **no** flag to disable it). +- **Edit** — pushed only if `this.options.acl.edit && !this.options.editDisabled`. +- **Unlink** — pushed only if `!this.options.unlinkDisabled`. +- **Remove** — pushed only if `this.options.acl.delete && !this.options.removeDisabled`. + +The panel (`client/src/views/record/panels/relationship.js`) feeds these flags from the panel metadata and lets you swap the whole row-actions view: + +```js +this.rowActionsView = this.defs.readOnly ? false : (this.defs.rowActionsView || this.rowActionsView); +// ... +rowActionsOptions: { + unlinkDisabled: unlinkDisabled, + editDisabled: this.defs.editDisabled, + removeDisabled: this.defs.removeDisabled, +}, +``` + +Consequence: +- `editDisabled: true` and `removeDisabled: true` (in `clientDefs..relationshipPanels.cTags`) would hide **Edit** and **Remove** — but **not View**, because View is hard-coded. +- To leave **only Unlink**, we point the panel's **`rowActionsView`** (a supported `relationshipPanels.` parameter) at a small **custom row-actions view** that returns only the Unlink action. This removes View, Edit and Remove in one go and is the clean, upgrade-safe approach. + +Custom client views live under `client/custom/src/...` and are referenced with the **`custom:`** prefix (per the official EspoCRM docs, "Custom views"). No build step is required; a cache rebuild plus a hard browser reload picks them up. + +### Confidence / self-verify before relying on it + +The action name `unlinkRelated`, the four-action structure, and the `rowActionsView` parameter are taken from the 9.3.8 source. Before relying on them, confirm against this instance: + +``` +podman exec espocrm_ctr sh -lc "grep -n 'rowActionsView\|unlinkDisabled\|editDisabled\|removeDisabled' /var/www/html/client/src/views/record/panels/relationship.js" +podman exec espocrm_ctr sh -lc "grep -n 'quickView\|quickEdit\|unlinkRelated\|removeRelated' /var/www/html/client/src/views/record/row-actions/relationship.js" +``` + +You should see View pushed unconditionally, Unlink guarded by `unlinkDisabled`, and the panel reading `this.defs.rowActionsView`. If this version differs, adjust the custom view's `getActionList()` to match the real action name(s) for "Unlink". + +## Steps + +1. **Confirm container and link name.** + - `podman ps` → identify the web container (expected `espocrm_ctr`). + - Confirm each of `Opportunity`, `Contact`, `Account` has a many-to-many link named `cTags`: + ``` + podman exec espocrm_ctr sh -lc "grep -rn 'cTags' /var/www/html/custom/Espo/Custom/Resources/metadata/entityDefs/" + ``` + +2. **Create the custom row-actions view.** Create the directory and file (inside the web container, on the persisted `/var/www/html`): + + `/var/www/html/client/custom/src/views/record/row-actions/tags-unlink-only.js` + + ```js + define('custom:views/record/row-actions/tags-unlink-only', + ['views/record/row-actions/relationship'], (RelationshipRowActionsView) => { + + return class extends RelationshipRowActionsView { + + getActionList() { + const list = []; + + // Keep only the "Unlink" action (German UI: "Link entfernen"). + // View / Edit / Remove are intentionally omitted. + if (!this.options.unlinkDisabled) { + list.push({ + action: 'unlinkRelated', + label: 'Unlink', + data: { + id: this.model.id, + }, + groupIndex: 0, + }); + } + + // Preserve any explicitly configured extra row actions (none by default). + this.getAdditionalActionList().forEach(item => list.push(item)); + + return list; + } + }; + }); + ``` + + Make sure the directories exist, e.g.: + ``` + podman exec espocrm_ctr sh -lc "mkdir -p /var/www/html/client/custom/src/views/record/row-actions" + ``` + +3. **Point the three Tags panels at the custom view.** For `Opportunity`, `Contact`, and `Account`, add the `rowActionsView` key to the existing `cTags` panel in: + `/var/www/html/custom/Espo/Custom/Resources/metadata/clientDefs/.json` + + The resulting `cTags` block must look like this (note: **keep** the existing `"create": false` from the previous task): + + ```json + { + "relationshipPanels": { + "cTags": { + "create": false, + "rowActionsView": "custom:views/record/row-actions/tags-unlink-only" + } + } + } + ``` + + - Target files: + - `/var/www/html/custom/Espo/Custom/Resources/metadata/clientDefs/Opportunity.json` + - `/var/www/html/custom/Espo/Custom/Resources/metadata/clientDefs/Contact.json` + - `/var/www/html/custom/Espo/Custom/Resources/metadata/clientDefs/Account.json` + - **MERGE into the existing JSON.** Preserve all other keys; in particular keep `relationshipPanels.cTags.create = false`. Only add the single `rowActionsView` key inside `relationshipPanels.cTags`. Do not overwrite the file. Produce valid JSON. + +4. **Fix ownership** so the web user owns the new/changed files (run as root inside the container): + ``` + podman exec espocrm_ctr chown -R www-data:www-data /var/www/html/client/custom /var/www/html/custom/Espo/Custom/Resources/metadata/clientDefs + ``` + +5. **Clear cache and rebuild** (run as the web user): + ``` + podman exec -u www-data espocrm_ctr php command.php rebuild + ``` + If `command.php` is not present in this version, use: + ``` + podman exec -u www-data espocrm_ctr bin/command rebuild + ``` + (Alternatively, an admin can run Administration → Rebuild in the web UI.) + +6. **Verify in the browser** (do a hard reload, Ctrl+Shift+R, to drop the client cache; ideally test as a regular non-admin user, and also as admin): + - Open an **Opportunity**, a **Contact**, and an **Account** record. On the **Tags** panel, open the ▾ dropdown of an attached tag. It must now show **only "Link entfernen" (Unlink)**. **Ansehen / Bearbeiten / Löschen** (View / Edit / Remove) must be gone. + - **"Link entfernen"** must still work (it detaches the tag from the record but does not delete the Tag itself). + - The **Tag** entity's own list/detail views must still allow viewing, editing and deleting tags normally (this task does not touch the Tag entity or any ACL). + +## Rollback + +1. Remove the `rowActionsView` key from `relationshipPanels.cTags` in the three `clientDefs` files (leave `create: false` if you still want the +button hidden). +2. Optionally delete `/var/www/html/client/custom/src/views/record/row-actions/tags-unlink-only.js`. +3. Rebuild again (step 5) and hard-reload the browser. diff --git a/espocrm-tags-select-only-claude-code.md b/espocrm-tags-select-only-claude-code.md new file mode 100644 index 0000000..b769729 --- /dev/null +++ b/espocrm-tags-select-only-claude-code.md @@ -0,0 +1,82 @@ +# Task: Disable the inline "Create" button on the Tags relationship panels in EspoCRM + +## Goal + +This EspoCRM instance has a custom many-to-many relationship to a custom `Tag` entity. It is exposed as a **"Tags" bottom relationship panel** on three entity types: **Opportunity**, **Contact**, and **Account**. On each of these three entities the link is named **`cTags`**. + +Right now the Tags panel offers a **Create (+)** button plus a **three-dots (⋯) menu** that contains the actions **View list** and **Select**. We want to **remove only the inline Create (+) button** on these Tags panels, so that users can attach **existing** Tag records only (a controlled tag vocabulary), using the **⋯ → Select** action. Creating new tags must still be possible, but **only** via the `Tag` entity's own list view — never inline from a record's Tags panel. This must apply to **all** users, including regular (non-admin) users. + +Do **not** change anything else (no other panels, fields, ACLs, or entities). + +## Environment + +- EspoCRM runs as a **rootless Podman pod** named `espocrm_pod` on this server. The web container is expected to be `espocrm_ctr` (confirm with `podman ps`). +- EspoCRM application root inside the web container: `/var/www/html`. The app runs as user **`www-data`**. +- Customizations live under `/var/www/html/custom/Espo/Custom/Resources/metadata/` and survive upgrades. This path must be on the **persisted** volume/bind-mount so the change is not lost when the container is recreated. + +## Mechanism + +EspoCRM reads relationship-panel options from `clientDefs..relationshipPanels.`. Setting `"create": false` for a panel hides its Create (+) button while keeping the ⋯ menu actions (View list / Select). EspoCRM **deep-merges** metadata, so we add only this single key per entity and do not redefine the whole panel. + +Confidence note on the key name: it is almost certainly `create`. Before relying on it, verify against this version's frontend source, e.g.: + +``` +grep -rn "'create'\|\"create\"\|create:" /var/www/html/client/src/views/record/panels/relationship.js +grep -rn "relationshipPanels" /var/www/html/client/src/views/record/ +``` + +Look at how the relationship panel decides whether to show the create action. If this version uses a different key (e.g. `createDisabled`), use the verified key instead and adjust the JSON below accordingly. + +## Steps + +1. **Confirm container and link name.** + - `podman ps` → identify the web container (expected `espocrm_ctr`). + - Confirm each of `Opportunity`, `Contact`, `Account` has a many-to-many link named `cTags` pointing to entity `Tag`, e.g.: + ``` + podman exec espocrm_ctr sh -lc "grep -rn 'cTags' /var/www/html/custom/Espo/Custom/Resources/metadata/entityDefs/" + ``` + +2. **Add the override for each of the three entities.** For `Opportunity`, `Contact`, and `Account`, ensure the file + `/var/www/html/custom/Espo/Custom/Resources/metadata/clientDefs/.json` + contains this key: + + ```json + { + "relationshipPanels": { + "cTags": { + "create": false + } + } + } + ``` + + - Target files: + - `/var/www/html/custom/Espo/Custom/Resources/metadata/clientDefs/Opportunity.json` + - `/var/www/html/custom/Espo/Custom/Resources/metadata/clientDefs/Contact.json` + - `/var/www/html/custom/Espo/Custom/Resources/metadata/clientDefs/Account.json` + - **If a file already exists, MERGE this key into the existing JSON** (preserve all other keys; if `relationshipPanels` already exists, add `cTags` to it). Do not overwrite. + - If a file or the directory does not exist, create it. Produce valid JSON. + +3. **Fix ownership** so the web user owns the new/changed files (run as root inside the container): + ``` + podman exec espocrm_ctr chown -R www-data:www-data /var/www/html/custom/Espo/Custom/Resources/metadata/clientDefs + ``` + +4. **Clear cache and rebuild** (run as the web user): + ``` + podman exec -u www-data espocrm_ctr php command.php rebuild + ``` + If `command.php` is not present in this version, use: + ``` + podman exec -u www-data espocrm_ctr bin/command rebuild + ``` + (Alternatively, an admin can run Administration → Rebuild in the web UI.) + +5. **Verify in the browser** (ideally as a regular non-admin user, and also as admin): + - Open an Opportunity, a Contact, and an Account record. The **Tags** panel must now have **no Create (+) button**. The three-dots (⋯) menu must still offer **View list** and **Select**, so existing tags can still be attached via **⋯ → Select**. + - The **Tag** entity's own list view must still have its Create button (deliberate tag creation still works there). + - Attaching an existing tag via **⋯ → Select** must still work. + +## Rollback + +Remove the added `relationshipPanels.cTags` key from the three `clientDefs` files (or delete the files if they contained only this key), then rebuild again. diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..5751ef6 --- /dev/null +++ b/plan.md @@ -0,0 +1,158 @@ +# Install EspoCRM on DesTEngSsv006 (rootless Podman pod) + +Goal: create four shell scripts that run EspoCRM as a rootless Podman pod under the local user `mkt`. + +A reference script, `create_pod_traefik.sh`, is attached. Follow its structure and conventions (the "House style" section below). + +## Target environment (given — do not provision in the scripts) + +- Host: DesTEngSsv006, Debian GNU/Linux 12 (bookworm), Kernel 6.1.0-49-amd64, x86-64. +- Podman 4.3.1, rootless. User `mkt` exists; subuid/subgid and `enable-linger` are configured. All scripts run as user `mkt`. +- Scripts live in `/home/mkt/bin` (a private git repo, not public, classified as a company secret). Plaintext passwords in the scripts are therefore deliberately acceptable. +- **Autostart:** Podman 4.3.1 has **no Quadlet** (introduced in 4.4). Use `podman generate systemd --files --new` (systemd `--user`) for autostart. Do not use Quadlet. + +## House style (mandatory — follow `create_pod_traefik.sh`) + +- Shebang `#!/bin/bash`, short comment header (who runs it, what it does). +- A block of environment variables at the top (pod name, container names, images, ports, IPs, `BIND_DIR`, systemd paths). +- Network option exactly as in the reference: `NET_OPTS='slirp4netns:allow_host_loopback=true,port_handler=slirp4netns'`. +- Data stored as bind mounts under `BIND_DIR="$HOME/.local/share/$POD_NAME"`, passed via `-v` in the `podman` command (no named volumes). +- Create the pod guarded with `if ! podman pod exists`; publish ports on the pod via `-p`. +- Create containers with `podman run -d --name ... --pod "$POD_NAME" -v ... "$IMAGE"`. +- Generate systemd units with `cd "$USER_SYSTEMD_DIR"; podman generate systemd --files --new --name "$POD_NAME"`. +- Then stop and remove the live pod, and `systemctl --user enable --now` the generated services (so the systemd-managed instance is what runs in the end). +- Status messages via `echo "... (rc=$?)"` as in the reference. + +## Fixed parameters + +- Pod name: `espocrm_pod`. +- Containers: `mariadb_ctr` (DB), `espocrm_ctr` (web), `espocrm_daemon_ctr` (scheduler/cron). **No WebSocket container** (no live updates). +- Images (pinned, no auto-updates): + - EspoCRM: `docker.io/espocrm/espocrm:9.3.8` (Apache variant, amd64). + - MariaDB: `docker.io/library/mariadb:11.4.12` (LTS, pinned patch version; separate from the EspoCRM image). +- Ports (loopback only): + - `127.0.0.1:8093` → EspoCRM web (container port 80). + - `127.0.0.1:8094` → MariaDB (container port 3306), for host-side DB access. +- MariaDB charset: utf8mb4, collation `utf8mb4_unicode_ci` (from the start, to avoid a later collation migration). +- `siteUrl`: `http://127.0.0.1:8093`. +- Admin user `admin`. DB name/user `espocrm`/`espocrm`. Passwords in plaintext in the scripts (private repo). Generate strong random passwords (e.g. `openssl rand -base64 24`) and set them as fixed variable values. + +## Data storage (bind mounts) + +``` +BIND_DIR="$HOME/.local/share/espocrm_pod" +DB_DATA_DIR="$BIND_DIR/mariadb" # -> /var/lib/mysql +ESPO_DATA_DIR="$BIND_DIR/espocrm" # -> /var/www/html (app, data/upload, custom, config.php) +``` + +Mount the bind mounts plain, **without `:U` and without `:Z`**. Both official images run their entrypoint as root and chown their own data directories to the in-container service user before dropping privileges: MariaDB chowns `/var/lib/mysql` to `mysql`, EspoCRM chowns its writable resources (`data`, `custom`, `client/custom`, `install/config.php`) to `www-data`. In rootless Podman the container root maps to host user `mkt` and holds `CAP_CHOWN` inside the user namespace, so those chowns succeed on the bind-mounted host directories. No SELinux on Debian, so no `:Z`. This matches the other rootless pods on this host. + +`espocrm_ctr` and `espocrm_daemon_ctr` share the same `ESPO_DATA_DIR` (both mount `/var/www/html`). + +## Container configuration + +### mariadb_ctr (start first) +Env variables: +``` +MARIADB_ROOT_PASSWORD= +MARIADB_DATABASE=espocrm +MARIADB_USER=espocrm +MARIADB_PASSWORD= +``` +Image arguments (utf8mb4) after the image name: +``` +--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci +``` + +### espocrm_ctr (web, start after the DB) +Env variables: +``` +ESPOCRM_DATABASE_PLATFORM=Mysql +ESPOCRM_DATABASE_HOST=127.0.0.1 +ESPOCRM_DATABASE_PORT=3306 +ESPOCRM_DATABASE_NAME=espocrm +ESPOCRM_DATABASE_USER=espocrm +ESPOCRM_DATABASE_PASSWORD= +ESPOCRM_ADMIN_USERNAME=admin +ESPOCRM_ADMIN_PASSWORD= +ESPOCRM_SITE_URL=http://127.0.0.1:8093 +ESPOCRM_CONFIG_USE_WEB_SOCKET=false +``` +Note: containers in a pod share the network namespace, so the DB is reachable at `127.0.0.1:3306` (not via the host port 8094). Use `127.0.0.1` instead of `localhost` to force TCP rather than a socket. + +### espocrm_daemon_ctr (scheduler, start last) +- Same image `docker.io/espocrm/espocrm:9.3.8`. +- Start with `--entrypoint docker-daemon.sh` (cron/scheduler loop). +- Mounts the same `ESPO_DATA_DIR` at `/var/www/html`. +- Reads its configuration from the `data/config.php` written by `espocrm_ctr` during install, so start it after the web container. + +## Script 1: create_pod_espocrm.sh (create plus autostart) + +Flow: +1. Env-var block (see above) including the pinned images, ports, `NET_OPTS`, `BIND_DIR`, `USER_SYSTEMD_DIR="$HOME/.config/systemd/user"`, and the plaintext passwords. +2. Create directories: `mkdir -p "$DB_DATA_DIR" "$ESPO_DATA_DIR" "$USER_SYSTEMD_DIR"`. +3. Create the pod (guarded with `if ! podman pod exists`): + ``` + podman pod create -n "$POD_NAME" --network "$NET_OPTS" \ + -p 127.0.0.1:8093:80 \ + -p 127.0.0.1:8094:3306 + ``` +4. Start `mariadb_ctr` (`podman rm -f` first for idempotency), with env, `-v "$DB_DATA_DIR":/var/lib/mysql`, then the utf8mb4 image arguments. +5. Wait for the DB to be ready: + ``` + for i in $(seq 1 60); do + podman exec "$DB_CTR" mariadb-admin ping --silent >/dev/null 2>&1 && break + sleep 2 + done + ``` +6. Start `espocrm_ctr` (`podman rm -f` first), with env, `-v "$ESPO_DATA_DIR":/var/www/html`. +7. Wait for EspoCRM install/response (HTTP 200/302 on `http://127.0.0.1:8093/`, max ~3 min). +8. Start `espocrm_daemon_ctr` (`podman rm -f` first), with `--entrypoint docker-daemon.sh`, `-v "$ESPO_DATA_DIR":/var/www/html`. +9. Generate systemd units: `cd "$USER_SYSTEMD_DIR"; podman generate systemd --files --new --name "$POD_NAME"` (produces `pod-espocrm_pod.service` plus `container-mariadb_ctr.service`, `container-espocrm_ctr.service`, `container-espocrm_daemon_ctr.service`). +10. **Enforce start order** (like the readiness injection in `create_pod_traefik.sh`): insert an `ExecStartPre` wait loop into `container-espocrm_ctr.service` and `container-espocrm_daemon_ctr.service` that waits for the dependency it needs (web waits for DB port `127.0.0.1:8094`; daemon waits for web port `127.0.0.1:8093`). Insert via `awk`, as in the reference script. +11. Stop and remove the live pod: `podman pod stop --time 15 "$POD_NAME"; podman pod rm -f --ignore "$POD_NAME"`. +12. Enable the services: + ``` + systemctl --user daemon-reload + systemctl --user enable --now pod-${POD_NAME}.service + systemctl --user enable --now container-mariadb_ctr.service + systemctl --user enable --now container-espocrm_ctr.service + systemctl --user enable --now container-espocrm_daemon_ctr.service + ``` +13. Print closing hints (status and log commands, as in the reference script). + +## Script 2: stop_pod_espocrm.sh + +- Stops the systemd-managed instance: `systemctl --user stop container-espocrm_daemon_ctr.service container-espocrm_ctr.service container-mariadb_ctr.service pod-${POD_NAME}.service` (in this order), with `echo` status messages. +- Do not disable (autostart stays in place), only stop. + +## Script 3: reset_pod_espocrm.sh (dangerous — deletes all data) + +- **Extra safety prompt**: only proceed after the exact confirmation phrase is entered, e.g.: + ``` + read -r -p "WARNING: this deletes the pod, containers, and ALL data under $BIND_DIR. To confirm, type 'RESET espocrm_pod': " C + [ "$C" = "RESET espocrm_pod" ] || { echo "Aborted."; exit 1; } + ``` +- Then: `systemctl --user disable --now` for the pod and all three containers; `podman pod rm -f --ignore "$POD_NAME"`; delete the generated unit files in `$USER_SYSTEMD_DIR`; `systemctl --user daemon-reload`; finally `rm -rf "$BIND_DIR"` (data gone). +- Print a hint that `create_pod_espocrm.sh` must be run again afterwards. + +## Script 4: backup_espocrm.sh (no timer/cron) + +- Backup target `BAK_DIR="$HOME/bak"` (`mkdir -p` at the start). +- Timestamp `TS=$(date +%Y%m%d-%H%M%S)`. +- DB dump via `podman exec` (so the backup script needs no password; it uses the container's own env): + ``` + podman exec "$DB_CTR" sh -c 'exec mariadb-dump --single-transaction --routines --triggers -uroot -p"$MARIADB_ROOT_PASSWORD" espocrm' | gzip > "$BAK_DIR/espocrm-db-$TS.sql.gz" + ``` + If `$MARIADB_ROOT_PASSWORD` is not available in the exec session, alternatively go host-side via the port: `mariadb-dump -h127.0.0.1 -P8094 -uroot -p ...` (password as a plaintext variable here too, same repo model). That is what port 8094 is for. +- File backup of the EspoCRM data (contains `config.php` with the crypt/passwordSalt key, `data/upload`, `custom`): + ``` + tar -czf "$BAK_DIR/espocrm-files-$TS.tar.gz" -C "$ESPO_DATA_DIR" . + ``` +- Closing message with the created file paths. +- No timer, no cron job (manual invocation). + +## Sources for verifying env variable names and entrypoints + +- Official image and tags: https://hub.docker.com/r/espocrm/espocrm +- Official Docker build repo (env variables, `docker-daemon.sh`): https://github.com/espocrm/espocrm-docker diff --git a/readme.md b/readme.md index 298ff34..24386e2 100644 --- a/readme.md +++ b/readme.md @@ -1 +1,275 @@ -user mkt folder /home/mkt/bin git repository +# EspoCRM on DesTEngSsv006 — rootless Podman pod + +This repository contains the shell scripts that run **EspoCRM** as a **rootless +Podman pod** under the local user `mkt`, plus the version-controlled snapshot of +all GUI customizations. Everything is driven by a handful of `*.sh` scripts; you +do not edit anything inside the running container by hand. + +> This is a **private** repository classified as a company secret. Plaintext +> passwords live directly in `create_pod_espocrm.sh` on purpose — keep the repo +> private. + +--- + +## 1. What gets deployed + +`create_pod_espocrm.sh` builds one pod with three containers: + +| Container | Image | Role | +|-----------------------|-----------------------------------------|---------------------------------------| +| `mariadb_ctr` | `docker.io/library/mariadb:11.4.12` | Database (utf8mb4 / utf8mb4_unicode_ci) | +| `espocrm_ctr` | `docker.io/espocrm/espocrm:9.3.8` | Web app (Apache) | +| `espocrm_daemon_ctr` | `docker.io/espocrm/espocrm:9.3.8` | Scheduler / cron loop | + +Pod name: **`espocrm_pod`**. Images are **pinned** (no auto-updates). + +**Ports — loopback only** (reach them via an SSH tunnel or a local reverse proxy): + +| Host address | → | Container | Purpose | +|-----------------------|----|-----------|-------------------------------| +| `127.0.0.1:8093` | → | `:80` | EspoCRM web UI | +| `127.0.0.1:8094` | → | `:3306` | MariaDB (host-side DB access) | + +Web UI: **http://127.0.0.1:8093** · Admin user: **`admin`** (password = the +`ESPOCRM_ADMIN_PASSWORD` value in `create_pod_espocrm.sh`). + +**Data lives in bind mounts on the host** (not in named volumes): + +``` +~/.local/share/espocrm_pod/mariadb -> /var/lib/mysql (database files) +~/.local/share/espocrm_pod/espocrm -> /var/www/html (app, uploads, config, customizations) +``` + +**Autostart:** Podman 4.3.1 has no Quadlet, so the scripts generate +`systemd --user` units (`podman generate systemd --files --new`) and enable them. +The systemd-managed instance is what actually runs after provisioning. + +--- + +## 2. The scripts at a glance + +| Script | What it does | +|------------------------------|-----------------------------------------------------------------------------| +| `create_pod_espocrm.sh` | Create/recreate the pod, deploy GUI customizations, rebuild, enable autostart. **Safe to re-run.** | +| `stop_pod_espocrm.sh` | Stop the running containers + pod (autostart stays enabled). | +| `backup_espocrm.sh` | Write a timestamped DB dump + a tarball of the app data dir to `~/bak`. | +| `restore_espocrm.sh` | Restore a backup set (DB + files). `--list` shows sets, `--yes` skips the prompt. | +| `snapshot_espocrm_custom.sh` | Refresh the `espocrm-custom/` snapshot from the running pod after GUI edits. | +| `reset_pod_espocrm.sh` | **DANGER:** delete the pod, units **and all data**. Requires typing a phrase.| +| `create_pod_traefik.sh` | Unrelated reference script (Traefik pod) — kept as the house-style template. | + +All scripts are run **as user `mkt`**, from this directory: + +```bash +cd ~/bin +./create_pod_espocrm.sh +``` + +--- + +## 3. First-time setup on a freshly installed host + +The scripts assume the rootless-Podman groundwork is already in place. On a brand +new host, verify these **prerequisites** first (one-time, usually needs root): + +1. **Podman installed** (4.3.x is what this was built for): + ```bash + podman --version + ``` +2. **User `mkt` exists** and has **subuid/subgid ranges** (needed for rootless + user-namespace mapping): + ```bash + grep '^mkt:' /etc/subuid /etc/subgid # each should print a line + # if missing (as root): usermod --add-subuids 524288-589823 --add-subgids 524288-589823 mkt + ``` +3. **Linger enabled** for `mkt`, so the user's systemd services start at boot and + keep running without an active login: + ```bash + loginctl show-user mkt -p Linger # want: Linger=yes + # if not: sudo loginctl enable-linger mkt + ``` +4. **A user systemd session is reachable.** When working over plain SSH you may + need: + ```bash + export XDG_RUNTIME_DIR=/run/user/$(id -u) + systemctl --user status # should respond + ``` + +Then, as `mkt`: + +```bash +cd ~/bin +./create_pod_espocrm.sh +``` + +On this **first run** the script will: + +- pull the pinned images, +- run the EspoCRM installer against an empty database (creates the schema and the + `admin` user from the credentials in the script), +- deploy all GUI customizations from `espocrm-custom/`, +- run a cache rebuild, +- generate and enable the `systemd --user` services (autostart). + +When it finishes, open **http://127.0.0.1:8093** and log in as `admin`. + +> Reaching the UI from your workstation: it only listens on loopback. Use an SSH +> tunnel, e.g. `ssh -L 8093:127.0.0.1:8093 mkt@DesTEngSsv006`, then browse to +> `http://127.0.0.1:8093` locally. + +--- + +## 4. Everyday operations + +```bash +# Start (after a stop) — the systemd units stay enabled, so this also happens on boot: +systemctl --user start pod-espocrm_pod.service \ + container-mariadb_ctr.service container-espocrm_ctr.service container-espocrm_daemon_ctr.service + +# Stop everything (autostart remains enabled): +./stop_pod_espocrm.sh + +# Status: +systemctl --user status pod-espocrm_pod.service +systemctl --user status container-espocrm_ctr.service + +# Live logs: +journalctl --user -u container-espocrm_ctr.service -f +``` + +Re-running `./create_pod_espocrm.sh` at any time is safe: it recreates the +containers from the persisted data, redeploys customizations, and rebuilds. + +--- + +## 5. Backups and restore + +Backups are **manual** (no timer/cron). Each run writes two timestamped files to +`~/bak`: a gzipped SQL dump and a tarball of the app data dir (which includes +`config.php` with the crypt key, uploads, and customizations). + +```bash +./backup_espocrm.sh # creates ~/bak/espocrm-db-.sql.gz and ...-files-.tar.gz + +./restore_espocrm.sh --list # list available backup sets +./restore_espocrm.sh # restore the latest set (asks for confirmation) +./restore_espocrm.sh 20260606-1346 # restore a specific set by timestamp +``` + +`restore_espocrm.sh` checks the archives, drops & re-imports the database, and +replaces the app data dir, then rebuilds. **Take a backup before any risky +operation** (image update, reset, major change). + +--- + +## 6. GUI customizations (important workflow) + +All customizations made through the EspoCRM GUI (Entity Manager, Layout Manager, +Label Manager, custom fields/views, custom CSS, …) are stored in +**`espocrm-custom/`** in this repo and deployed by `create_pod_espocrm.sh`. This +snapshot is the **single source of truth** — a reset + reprovision restores it. + +Current customizations captured here include the `CTag` tag entity and its +relationships, the Tags panels (Create `+` button removed, row menu reduced to +*Unlink*), the signature-editor image button, and variable-width Markdown tables. + +**The golden rule:** after you change anything in the EspoCRM GUI, capture it: + +```bash +./snapshot_espocrm_custom.sh # pull current customizations into espocrm-custom/ +git add espocrm-custom && git commit -m "Update EspoCRM customizations snapshot" +``` + +If you skip this, the next `create_pod_espocrm.sh` run will **revert** your +un-captured GUI changes back to whatever is in `espocrm-custom/`. + +> Note: customizations are *files*. Your actual records (tags, opportunities, +> contacts, …) are *data* and live only in the database — protect those with +> `backup_espocrm.sh`, not the snapshot. + +--- + +## 7. Updating the container images + +Images are pinned, so updates are deliberate. **Always back up first.** + +```bash +./backup_espocrm.sh +``` + +### EspoCRM (e.g. 9.3.8 → a newer 9.x) + +1. Edit the version in `create_pod_espocrm.sh` (the web and daemon share the same + `ESPO_IMAGE` variable, so one change covers both): + ```bash + ESPO_IMAGE='docker.io/espocrm/espocrm:' + ``` +2. Pull and reprovision: + ```bash + podman pull docker.io/espocrm/espocrm: + ./create_pod_espocrm.sh + ``` + The EspoCRM container entrypoint **auto-runs its upgrade** when the image + version is newer than the installed version (it migrates the database and app + files on the bind mount). The script then redeploys customizations and + rebuilds. +3. Verify the UI and your customizations. If you tweaked anything in the GUI as + part of the upgrade, re-run `./snapshot_espocrm_custom.sh` and commit. + +### MariaDB (e.g. 11.4.12 → a newer 11.4.x patch) + +Patch/minor updates **within the same 11.4 LTS series** are data-compatible: + +```bash +# edit DB_IMAGE in create_pod_espocrm.sh, then: +podman pull docker.io/library/mariadb: +./create_pod_espocrm.sh +``` + +For a **major** MariaDB jump (e.g. 11.x → 12.x) do not just swap the tag — +back up, then dump/restore into a fresh data dir, and test. When in doubt, take +a backup and try it on a throwaway copy first. + +### After any image change + +```bash +podman ps # all three containers Up +curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8093/ # expect 200 +podman image prune # optionally drop the old images +``` + +Commit the bumped image tag(s) in `create_pod_espocrm.sh`. + +--- + +## 8. Resetting / uninstalling + +`reset_pod_espocrm.sh` **destroys the pod, the systemd units, and ALL data** +(database + uploads). It asks you to type `RESET espocrm_pod` to proceed. + +```bash +./backup_espocrm.sh # do this first if you might want the data back +./reset_pod_espocrm.sh +./create_pod_espocrm.sh # fresh install; customizations are restored from espocrm-custom/ +# (optional) restore your data: +./restore_espocrm.sh +``` + +After a reset, a fresh `create_pod_espocrm.sh` gives you a clean install with all +**customizations** back in place; **records** come back only via `restore`. + +--- + +## 9. Repository layout + +``` +create_pod_espocrm.sh # provision + autostart + deploy customizations +stop_pod_espocrm.sh # stop the pod +backup_espocrm.sh # back up DB + files to ~/bak +restore_espocrm.sh # restore a backup set +snapshot_espocrm_custom.sh # refresh espocrm-custom/ from the running pod +reset_pod_espocrm.sh # DANGER: delete pod + units + all data +espocrm-custom/ # version-controlled GUI customizations (source of truth) +create_pod_traefik.sh # unrelated reference script (house-style template) +*.md # task/instruction notes and this readme +``` diff --git a/snapshot_espocrm_custom.sh b/snapshot_espocrm_custom.sh new file mode 100755 index 0000000..412c604 --- /dev/null +++ b/snapshot_espocrm_custom.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# To be run by user mkt. Refreshes the version-controlled GUI-customization +# snapshot ($CUSTOM_SRC) from the running EspoCRM pod. Run this after making +# customizations in the EspoCRM GUI (Entity Manager, Layout Manager, Label +# Manager, etc.) so create_pod_espocrm.sh can restore them on reset+reprovision. +# Commit the result to the repo afterwards. + +# Environment variables +WEB_CTR='espocrm_ctr' +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +CUSTOM_SRC="$SCRIPT_DIR/espocrm-custom" # contains custom/ and client/custom/ + +if ! podman exec "$WEB_CTR" true 2>/dev/null; then + echo "ERROR: container $WEB_CTR is not running. Start the pod first." >&2 + exit 1 +fi + +# Pull /var/www/html/{custom,client/custom} into a clean snapshot dir. +rm -rf "$CUSTOM_SRC" +mkdir -p "$CUSTOM_SRC" +podman exec "$WEB_CTR" tar -C /var/www/html -cf - custom client/custom \ + | tar -C "$CUSTOM_SRC" -xf - --no-same-owner +echo "Refreshed snapshot at $CUSTOM_SRC (rc=$?)" + +echo "Files: $(find "$CUSTOM_SRC" -type f | wc -l)" +echo "Review and commit, e.g.: git -C \"$SCRIPT_DIR\" add espocrm-custom && git -C \"$SCRIPT_DIR\" commit -m 'Update EspoCRM customizations snapshot'"