it is a way …
for custom Service notes and for custom host notes:


OMD[test]:~$ cat "/opt/omd/sites/test/etc/check_mk/notes/services/APX-01/XIQ SSID Staging"
test by Bernd
explanation and howto is following
| Komponente |
File |
Function |
| Part 1 |
views/paint_custom_notes.py |
Monkey-Patch, show Notes + Edit-Button |
| Part 2 |
pages/custom_notes_editor.py |
Editor-Page: read, show, save, redirect |
View (Part 1) Editor (Part 2)
────────────────── ──────────────────────────
_my_paint_custom_notes() CustomNotesEditorPage.page()
→ reads notes files → reads/writes notes files
→ renders edit button ────→ → renders textarea
→ saves on POST
→ redirects back to view
You have to work with a is a Check_MK GUI patch (monkey-patch) that replaces the internal function _paint_custom_notes in the painter module at runtime with a custom implementation.
But be careful
Detailed Walkthrough
1. Imports
from cmk.gui.painter.v0 import painters
from cmk.gui.painter.v0.painters import match_path_entries_with_item
Two important imports from the CMK painter system. match_path_entries_with_item is an internal function that matches files in a directory against a host or service name. At the end of the script, painters._paint_custom_notes is overwritten directly – that is the actual monkey-patch.
2. _get_back_url_data(row)
url = request.url
view_name_match = re.findall(r"view_name=\w+", url)
view_name = (
str(view_name_match[0]).replace("view_name=", "")
if view_name_match
else "allhosts"
)
Reads the current URL from the HTTP request and extracts the view_name parameter via regex (e.g. allhosts, host, service). This is later used as the return URL for the editor, so that after saving the user is taken back to the correct view. Fallback is "allhosts".
Returns: Dictionary containing site and view_name.
3. _render_edit_icon_button(data)
url = makeuri_contextless(request,
filename="custom_notes_editor.py",
vars_=[("type", ...), ("item", ...), ...])
return html.render_a(
content=HTML(html.render_icon("edit", cssclass="iconbutton"), escape=False),
...
)
Builds a URL to custom_notes_editor.py with all required parameters (type, item, host, site, view name) and renders it as a clickable edit icon link. The icon (edit) is a standard CMK icon. target="main" opens the editor in the main frame of the CMK frameset.
Returns: A fully rendered HTML <a> tag as a CMK HTML object.
4. _format_item_to_valid_file_name(item)
return item.replace("/", "_slash_").replace(":", "_colon_")
Simple sanitisation: slashes and colons in the service name are replaced so the name can be used as a filename on the filesystem. A service like Filesystem /var/log becomes Filesystem _slash_var_slash_log.
5. _my_paint_custom_notes(what, row, config) – Core function
This is the actual replacement for the original CMK function. The flow:
Step 1 – Determine context:
if what == "service":
notes_dir = default_config_dir + "/notes/services"
dirs = match_path_entries_with_item([Path(notes_dir)], host)
item = svc
else:
dirs = [Path(default_config_dir) / "notes/hosts"]
item = host
Depending on whether what == "service" or "host", the correct notes directory is selected. For services, match_path_entries_with_item first looks for host-specific subdirectories inside the service notes directory. For hosts it points directly to the notes/hosts directory.
Step 2 – Normalise filename and find files:
item_ = _format_item_to_valid_file_name(item)
files = sorted(match_path_entries_with_item(dirs, item_), reverse=True)
The item name (host or service) is made filesystem-safe, then all matching notes files are found and sorted in descending order (newest / alphabetically last first).
Step 3 – Tag replacement:
def replace_tags(text: str) -> str:
sitename = row["site"]
url_prefix = get_site_config(config, sitename)["url_prefix"]
return (
text.replace("$URL_PREFIX$", url_prefix)
.replace("$SITE$", sitename)
.replace("$HOSTNAME$", host)
...
)
An inner function that replaces placeholders in the notes text with real values. This is the same variable system as in the original CMK Custom Notes – just now extensible. Supported tags:
| Tag |
Replaced with |
$URL_PREFIX$ |
URL prefix of the site |
$SITE$ |
Site name |
$HOSTNAME$ |
Hostname |
$HOSTNAME_LOWER$ |
Hostname in lowercase |
$HOSTNAME_UPPER$ |
Hostname in uppercase |
$HOSTNAME_TITLE$ |
Hostname in title case |
$HOSTADDRESS$ |
IP address |
$SERVICEOUTPUT$ |
Plugin output of the service |
$HOSTOUTPUT$ |
Plugin output of the host |
$SERVICEDESC$ |
Service description |
Step 4 – Read and assemble content:
for f in files:
contents.append(replace_tags(f.read_text(encoding="utf8").strip()))
All matching notes files are read, stripped, and processed through replace_tags.
Step 5 – Return value:
return "", _render_edit_icon_button(data) + "<hr>".join(contents)
Returns a CellSpec tuple: the first element is the CSS class name (empty), the second is the HTML content. The edit button comes first, followed by all notes contents separated by <hr>.
6. The Monkey-Patch
painters._paint_custom_notes = _my_paint_custom_notes
This last line is the actual intervention. CMK loads this script as a GUI extension (most likely from local/share/check_mk/web/plugins/views/) and overwrites the original function in the already-loaded painters module. From this point on, every painter call invokes the new function.