From e83d61d8a402004013bc771f947323457a3be459 Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 13 Mar 2025 23:48:22 +0100 Subject: [PATCH] docuemtation improvements --- .../msp_documentation/msp_documentation.js | 1 + msp/msp/doctype/it_object/it_object.json | 29 ++- .../msp_documentation/msp_documentation.js | 72 +++++- .../msp_documentation/msp_documentation.json | 42 ++-- msp/msp/print_format/__init__.py | 0 .../html_msp_documentation/__init__.py | 0 .../html_msp_documentation.json | 32 +++ msp/msp/workspace/technik/technik.json | 4 +- msp/tactical-rmm.py | 128 +++++++---- msp/tools.py | 211 ++++++++++++++++++ 10 files changed, 446 insertions(+), 73 deletions(-) create mode 100644 msp/doctype/msp_documentation/msp_documentation.js create mode 100644 msp/msp/print_format/__init__.py create mode 100644 msp/msp/print_format/html_msp_documentation/__init__.py create mode 100644 msp/msp/print_format/html_msp_documentation/html_msp_documentation.json diff --git a/msp/doctype/msp_documentation/msp_documentation.js b/msp/doctype/msp_documentation/msp_documentation.js new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/msp/doctype/msp_documentation/msp_documentation.js @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/msp/msp/doctype/it_object/it_object.json b/msp/msp/doctype/it_object/it_object.json index 6381bb7..588cc14 100644 --- a/msp/msp/doctype/it_object/it_object.json +++ b/msp/msp/doctype/it_object/it_object.json @@ -36,7 +36,10 @@ "monitoring_link", "oitc_host_uuid", "rmm_agent_id", - "rmm_instance" + "rmm_instance", + "documentation_section", + "visible_in_documentation", + "documentation_text" ], "fields": [ { @@ -215,14 +218,35 @@ "fieldname": "rmm_software", "fieldtype": "Markdown Editor", "label": "RMM Software" + }, + { + "collapsible": 1, + "collapsible_depends_on": "eval: !doc.documentation_text", + "fieldname": "documentation_section", + "fieldtype": "Section Break", + "label": "Documentation" + }, + { + "description": "This will be visible on the on the MSP Documentation", + "fieldname": "documentation_text", + "fieldtype": "Text Editor", + "label": "Documentation Text" + }, + { + "default": "0", + "description": "If checked, this object will be selected when creating a MSP Documentation.", + "fieldname": "visible_in_documentation", + "fieldtype": "Check", + "label": "visible in Documentation" } ], "image_field": "image", "links": [], - "modified": "2023-06-08 23:00:31.338542", + "modified": "2025-03-10 14:55:30.929828", "modified_by": "Administrator", "module": "MSP", "name": "IT Object", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -265,6 +289,7 @@ "search_fields": "title, main_ip, item", "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "title", "track_changes": 1, "track_views": 1 diff --git a/msp/msp/doctype/msp_documentation/msp_documentation.js b/msp/msp/doctype/msp_documentation/msp_documentation.js index 313c602..1d29415 100644 --- a/msp/msp/doctype/msp_documentation/msp_documentation.js +++ b/msp/msp/doctype/msp_documentation/msp_documentation.js @@ -5,22 +5,80 @@ frappe.ui.form.on('MSP Documentation', { refresh(frm) { frm.add_custom_button('1. Get Tactical Agents', function(){ + frappe.dom.freeze('Fetching Tactical Agents...'); frappe.call({ method: 'msp.tactical-rmm.get_agents_pretty', args: { documentation: frm.doc.name }, - callback:function(r){ - console.log(r.message) - frm.reload_doc() + callback: function(r) { + frappe.dom.unfreeze(); + if (r.exc) { + frappe.msgprint({ + title: __('Error'), + indicator: 'red', + message: __('Failed to fetch tactical agents. Please try again.') + }); + return; + } + frappe.show_alert({ + message: __('Successfully fetched tactical agents'), + indicator: 'green' + }); + frm.reload_doc(); } }); }, 'Workflow'); - frm.add_custom_button('2. office suche', function(){ + + frm.add_custom_button('2. Office Search', function(){ + frappe.dom.freeze('Searching for Office installations...'); frappe.call({ method: 'msp.tactical-rmm.search_office', args: { documentation: frm.doc.name }, - callback:function(r){ - console.log(r.message) - frm.reload_doc() + callback: function(r) { + frappe.dom.unfreeze(); + if (r.exc) { + frappe.msgprint({ + title: __('Error'), + indicator: 'red', + message: __('Failed to complete Office search. Please try again.') + }); + return; + } + frappe.show_alert({ + message: __('Office search completed'), + indicator: 'green' + }); + frm.reload_doc(); + } + }); + }, 'Workflow'); + + // Add new button for IT Objects documentation + frm.add_custom_button('3. Generate IT Objects', function(){ + frappe.dom.freeze('Generating IT Objects documentation...'); + frappe.call({ + method: 'msp.tools.get_documentation_html', + args: { + it_landscape: frm.doc.landscape + }, + callback: function(r) { + frappe.dom.unfreeze(); + if (r.exc) { + frappe.msgprint({ + title: __('Error'), + indicator: 'red', + message: __('Failed to generate IT Objects documentation. Please try again.') + }); + return; + } + if (r.message) { + frm.set_value('it_objects', r.message); + frm.save().then(() => { + frappe.show_alert({ + message: __('IT Objects documentation generated successfully'), + indicator: 'green' + }); + }); + } } }); }, 'Workflow'); diff --git a/msp/msp/doctype/msp_documentation/msp_documentation.json b/msp/msp/doctype/msp_documentation/msp_documentation.json index 5aaab09..9fea7c3 100644 --- a/msp/msp/doctype/msp_documentation/msp_documentation.json +++ b/msp/msp/doctype/msp_documentation/msp_documentation.json @@ -11,11 +11,12 @@ "customer", "customer_name", "tactical_rmm_tenant_caption", + "tactical_rmm_site_name", "generation_date", - "introduction", + "introduction_text", + "it_objects", "server_list", "workstation_list", - "system_list", "backup", "aditional_data" ], @@ -34,16 +35,6 @@ "options": "Customer", "read_only": 1 }, - { - "fieldname": "introduction", - "fieldtype": "Markdown Editor", - "label": "Introduction" - }, - { - "fieldname": "system_list", - "fieldtype": "Markdown Editor", - "label": "System List" - }, { "fetch_from": "landscape.tactical_rmm_tenant_caption", "fieldname": "tactical_rmm_tenant_caption", @@ -52,7 +43,7 @@ }, { "fieldname": "backup", - "fieldtype": "Markdown Editor", + "fieldtype": "Text Editor", "label": "Backup" }, { @@ -67,12 +58,12 @@ }, { "fieldname": "server_list", - "fieldtype": "Markdown Editor", + "fieldtype": "Text Editor", "label": "Server List" }, { "fieldname": "workstation_list", - "fieldtype": "Markdown Editor", + "fieldtype": "Text Editor", "label": "Workstation List" }, { @@ -81,11 +72,27 @@ "fieldtype": "Data", "label": "Customer Name", "read_only": 1 + }, + { + "description": "Explain: What is the target of this particular Documentation? Give an over the solution you are describing.", + "fieldname": "introduction_text", + "fieldtype": "Text Editor", + "label": "Introduction Text" + }, + { + "fieldname": "it_objects", + "fieldtype": "Text Editor", + "label": "IT Objects" + }, + { + "fieldname": "tactical_rmm_site_name", + "fieldtype": "Data", + "label": "Tactical RMM Site Name" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-06-09 07:14:25.256192", + "modified": "2025-03-11 08:06:34.303790", "modified_by": "Administrator", "module": "MSP", "name": "MSP Documentation", @@ -105,5 +112,6 @@ } ], "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/msp/msp/print_format/__init__.py b/msp/msp/print_format/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/msp/msp/print_format/html_msp_documentation/__init__.py b/msp/msp/print_format/html_msp_documentation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/msp/msp/print_format/html_msp_documentation/html_msp_documentation.json b/msp/msp/print_format/html_msp_documentation/html_msp_documentation.json new file mode 100644 index 0000000..ac2d2aa --- /dev/null +++ b/msp/msp/print_format/html_msp_documentation/html_msp_documentation.json @@ -0,0 +1,32 @@ +{ + "absolute_value": 0, + "align_labels_right": 0, + "creation": "2025-03-11 00:26:31.608123", + "custom_format": 0, + "default_print_language": "de", + "disabled": 0, + "doc_type": "MSP Documentation", + "docstatus": 0, + "doctype": "Print Format", + "font": "Default", + "font_size": 0, + "format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"
\\r\\n\\t
\\r\\n\\t\\t
\\r\\n\\t\\t\\t\\r\\n\\t\\t
\\r\\n\\t
\\r\\n\\t
\\r\\n\\t\\t

\\r\\nIT Documentation\\r\\n\\t\\t
{{ doc.name }}

\\r\\n\\t
\\r\\n\\t
\\r\\n\\t\\t\\r\\n\\t
\\r\\n
\\r\\n
\\r\\n\\t
\\r\\n\\t\\t
\\r\\n\\t
\\r\\n
\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"customer\", \"print_hide\": 0, \"label\": \"Customer\"}, {\"fieldname\": \"customer_name\", \"print_hide\": 0, \"label\": \"Customer Name\"}, {\"fieldname\": \"generation_date\", \"print_hide\": 0, \"label\": \"Generation Date\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"landscape\", \"print_hide\": 0, \"label\": \"Landscape\"}, {\"fieldname\": \"tactical_rmm_tenant_caption\", \"print_hide\": 0, \"label\": \"Tactical RMM Tenant Caption\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"

Introduction

\"}, {\"fieldname\": \"introduction_text\", \"print_hide\": 0, \"label\": \"Introduction Text\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"
\"}, {\"fieldtype\": \"Section Break\", \"label\": \"IT Objects\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"it_objects\", \"print_hide\": 0, \"label\": \"IT Objects\"}, {\"fieldtype\": \"Section Break\", \"label\": \"Systems from RMM\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"

Server Systems

\\n
\"}, {\"fieldname\": \"server_list\", \"print_hide\": 0, \"label\": \"Server List\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"

Workstation Systems

\\n
\"}, {\"fieldname\": \"workstation_list\", \"print_hide\": 0, \"label\": \"Workstation List\"}, {\"fieldtype\": \"Section Break\", \"label\": \"Backups\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"backup\", \"print_hide\": 0, \"label\": \"Backup\"}, {\"fieldname\": \"aditional_data\", \"print_hide\": 0, \"label\": \"Aditional Data\"}]", + "idx": 0, + "line_breaks": 0, + "margin_bottom": 0.0, + "margin_left": 0.0, + "margin_right": 0.0, + "margin_top": 0.0, + "modified": "2025-03-11 00:27:41.672487", + "modified_by": "D.Malinowski@itsdave.de", + "module": "MSP", + "name": "HTML MSP Documentation", + "owner": "D.Malinowski@itsdave.de", + "page_number": "Hide", + "print_format_builder": 1, + "print_format_builder_beta": 0, + "print_format_type": "Jinja", + "raw_printing": 0, + "show_section_headings": 1, + "standard": "Yes" +} \ No newline at end of file diff --git a/msp/msp/workspace/technik/technik.json b/msp/msp/workspace/technik/technik.json index 408626d..10f81bb 100644 --- a/msp/msp/workspace/technik/technik.json +++ b/msp/msp/workspace/technik/technik.json @@ -77,7 +77,7 @@ "type": "Link" } ], - "modified": "2024-07-30 07:36:09.918509", + "modified": "2024-10-11 10:44:54.148354", "modified_by": "D.Malinowski@itsdave.de", "module": "MSP", "name": "Technik", @@ -87,7 +87,7 @@ "public": 1, "quick_lists": [], "roles": [], - "sequence_id": 2.0, + "sequence_id": 3.0, "shortcuts": [], "title": "Technik" } \ No newline at end of file diff --git a/msp/tactical-rmm.py b/msp/tactical-rmm.py index de743bc..609d637 100644 --- a/msp/tactical-rmm.py +++ b/msp/tactical-rmm.py @@ -5,6 +5,7 @@ import requests import json from pprint import pprint import re +from .tools import render_card_html, render_single_card @frappe.whitelist() def get_agents(it_landscape, rmm_instance = None, tactical_rmm_tenant_caption = None): @@ -88,36 +89,70 @@ def get_agents_pretty(documentation): documentation_doc = frappe.get_doc("MSP Documentation", documentation) if not documentation_doc.tactical_rmm_tenant_caption: - frappe.throw("Tennant Caption missing") + frappe.throw("Tenant Caption missing") client_name = documentation_doc.tactical_rmm_tenant_caption - + site_name = documentation_doc.tactical_rmm_site_name agents = get_all_agents() - print(agents) - agent_list = [] - workstation_list = [] - server_list = [] - #if not agent_list: - # frappe.throw("API Abfrage hat keine Agents geliefert.") - - - for agent in agents: - if agent["client_name"] == client_name: - agent_list.append(agent) - if agent["monitoring_type"] == "workstation": - workstation_list.append(agent) - if agent["monitoring_type"] == "server": - server_list.append(agent) - pprint(agent) - output = make_agent_md_output(agent_list) - output_workstation = make_agent_md_output(workstation_list) - output_server = make_agent_md_output(server_list) + # Filter and organize agents + agent_list = [] + for agent in agents: + # Filter by client_name and optionally by site_name if provided + if agent["client_name"] == client_name and (not site_name or agent["site_name"] == site_name): + # Format agent data for card rendering + agent_item = { + 'title': agent['hostname'], # Using hostname as title + 'type': agent['monitoring_type'], # workstation/server + 'ip': agent['local_ips'], + 'location': agent['site_name'], + 'metadata': { + 'Operating System': agent['operating_system'], + 'Hardware Model': render_model(agent['make_model']), + 'Serial Number': agent.get('serial_number', ''), + 'CPU': ", ".join(agent['cpu_model']) if isinstance(agent['cpu_model'], list) else agent['cpu_model'], + 'Graphics': agent['graphics'], + 'Storage': ", ".join(agent['physical_disks']) if isinstance(agent['physical_disks'], list) else agent['physical_disks'], + 'Public IP': agent['public_ip'], + 'Last Seen': agent['last_seen'], + 'Last User': agent['logged_username'] + }, + 'description': agent.get('description', '') + } + agent_list.append(agent_item) - documentation_doc.system_list = output - documentation_doc.workstation_list = output_workstation - documentation_doc.server_list = output_server + # Check if any agents were found + if not agent_list: + no_agents_message = f"
No agents found for client '{client_name}'" + if site_name: + no_agents_message += f" at site '{site_name}'" + no_agents_message += ".
" + + # Update the documentation with the message + documentation_doc.system_list = no_agents_message + documentation_doc.workstation_list = no_agents_message + documentation_doc.server_list = no_agents_message + documentation_doc.save() + + return [] + + # Generate HTML using the shared render_card_html function from tools.py + all_agents_html = render_card_html(agent_list, "tactical") + + # Filter lists for workstations and servers + workstation_list = [a for a in agent_list if a['type'].lower() == 'workstation'] + server_list = [a for a in agent_list if a['type'].lower() == 'server'] + + # Generate separate HTML for workstations and servers + workstation_html = render_card_html(workstation_list, "tactical") + server_html = render_card_html(server_list, "tactical") + + # Update the documentation + documentation_doc.system_list = all_agents_html + documentation_doc.workstation_list = workstation_html + documentation_doc.server_list = server_html documentation_doc.save() + return agent_list @@ -179,29 +214,32 @@ def get_patches_for_agent(agent_id=None): def make_agent_md_output(agents): - md_output = "" + # Prepare items for card rendering + items = [] for agent in agents: - md_output += f''' -#### {agent["hostname"]} -- OS: {agent["operating_system"]} -- CPU: {agent["cpu_model"]} -- GPU: {agent["graphics"]} -- Disks: {agent["physical_disks"]} -- Model: {render_model(agent["make_model"])} -- Serial Number: {agent["serial_number"]} -- Type: {agent["monitoring_type"]} -- Site: {agent["site_name"]} -- Local IPs: {agent["local_ips"]} -- Public IP: {agent["public_ip"]} -- Last Seen: {agent["last_seen"]} -- Last User: {agent["logged_username"]} -''' - if agent["description"]: - md_output += f'''Description: -{agent["description"]} -''' + items.append({ + 'title': agent['hostname'], + 'type': agent['monitoring_type'], + 'ip': agent['local_ips'], + 'location': agent['site_name'], + 'status': agent['status'], + 'metadata': { + 'OS': agent['operating_system'], + 'CPU': ", ".join(agent['cpu_model']) if isinstance(agent['cpu_model'], list) else agent['cpu_model'], + 'GPU': agent['graphics'], + 'Disks': ", ".join(agent['physical_disks']) if isinstance(agent['physical_disks'], list) else agent['physical_disks'], + 'Model': render_model(agent['make_model']), + 'Serial Number': agent.get('serial_number'), + 'Type': agent['monitoring_type'], + 'Site': agent['site_name'], + 'Public IP': agent['public_ip'], + 'Last Seen': agent['last_seen'], + 'Last User': agent['logged_username'] + }, + 'description': agent.get('description') + }) - return md_output + return render_card_html(items, "agent") def render_model(model): diff --git a/msp/tools.py b/msp/tools.py index 35dbc85..e516e45 100644 --- a/msp/tools.py +++ b/msp/tools.py @@ -620,6 +620,217 @@ def get_status_from_ticket(): print(f"{field}: {value}") print("---") +def render_card_html(items, item_type): + """ + Generate formatted HTML cards for displaying IT objects or RMM agents. + + Args: + items: List of dictionaries containing item data + item_type: Type of items ("it_object", "tactical", or "agent") + + Returns: + Formatted HTML string with cards + """ + html = ''' + + ''' + + # Group items by type if needed + if item_type == "tactical": + server_items = [i for i in items if i.get('type', '').lower() == 'server'] + workstation_items = [i for i in items if i.get('type', '').lower() == 'workstation'] + + html += '
' + if server_items: + html += '

Server Systems

' + html += ''.join(render_single_card(item) for item in server_items) + + if workstation_items: + html += '

Workstation Systems

' + html += ''.join(render_single_card(item) for item in workstation_items) + html += '
' + else: + html += ''.join(render_single_card(item) for item in items) + + return html + +def render_single_card(item): + """ + Generate HTML for a single card. + + Args: + item: Dictionary containing item data + + Returns: + HTML string for a single card + """ + # Get the basic information + title = item.get('title', '') + type_value = item.get('type', '').capitalize() + ip = item.get('ip', '') or item.get('metadata', {}).get('IP', '') or item.get('metadata', {}).get('Local IPs', '') + location = item.get('location', '') + + # Create a header that matches the previous style + header = f"{title}" + if type_value: + header += f" ({type_value})" + if ip: + header += f" - {ip}" + + html = f''' +
+

{header}

+
+
' + return html + +@frappe.whitelist() +def get_documentation_html(it_landscape): + """Generate formatted HTML cards for all IT objects in a landscape that are marked for documentation.""" + + it_objects = frappe.get_all( + "IT Object", + filters={ + "visible_in_documentation": 1, + "it_landscape": it_landscape + }, + fields=[ + "title", "type", "location", "location_full_path", + "status", "main_ip", "image", "documentation_text" + ] + ) + + if not it_objects: + return "

No documented IT objects found in this landscape.

" + + # Prepare items for card rendering + items = [] + for obj in it_objects: + items.append({ + 'title': obj.title, + 'type': obj.type, + 'ip': obj.main_ip, + 'location': obj.location_full_path or obj.location, + 'status': obj.status, + 'metadata': { + 'Type': obj.type, + 'Status': obj.status + }, + 'description': obj.documentation_text + }) + + return render_card_html(items, "it_object") +