docuemtation improvements

This commit is contained in:
Dave 2025-03-13 23:48:22 +01:00
parent 0bafeac13f
commit e83d61d8a4
10 changed files with 446 additions and 73 deletions

View File

@ -0,0 +1 @@

View File

@ -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

View File

@ -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()
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()
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');

View File

@ -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": []
}

View File

View File

@ -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\": \"<div class=\\\"row clearfix main\\\">\\r\\n\\t<div class=\\\"col-xs-5 center\\\">\\r\\n\\t\\t<div style=\\\"height: 75px;\\\">\\r\\n\\t\\t\\t<img src=\\\"/files/logo-1024x300.svg\\\" height=\\\"100%\\\">\\r\\n\\t\\t</div>\\r\\n\\t</div>\\r\\n\\t<div class=\\\"col-xs-5 center\\\" style=\\\"height: 75px; display: table;\\\">\\r\\n\\t\\t<p style=\\\"display: table-cell; vertical-align: middle; text-align: right; color: #333A3F; text-transform: uppercase; font-size: 22px;\\\">\\r\\nIT Documentation\\r\\n\\t\\t<br>{{ doc.name }}</p>\\r\\n\\t</div>\\r\\n\\t<div class=\\\"col-xs-2 center\\\">\\r\\n\\t\\t<img class=\\\"vcenter\\\" style=\\\"height: 75px;\\\" src=\\\"https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=https://erpnext.itsdave.de/app/msp-documentation/{{ doc.name }}&format=svg&color=333A3F\\\">\\r\\n\\t</div>\\r\\n</div>\\r\\n<div class=\\\"row\\\">\\r\\n\\t<div class=\\\"col-xs-12\\\">\\r\\n\\t\\t<hr style=\\\"border-color: #333A3F;\\\">\\r\\n\\t</div>\\r\\n</div>\"}, {\"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\": \"<h3>Introduction</h3>\"}, {\"fieldname\": \"introduction_text\", \"print_hide\": 0, \"label\": \"Introduction Text\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<hr>\"}, {\"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\": \"<h3>Server Systems</h3>\\n<hr>\"}, {\"fieldname\": \"server_list\", \"print_hide\": 0, \"label\": \"Server List\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<h3>Workstation Systems</h3>\\n<hr>\"}, {\"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"
}

View File

@ -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"
}

View File

@ -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)
# Filter and organize 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)
# 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)
output = make_agent_md_output(agent_list)
output_workstation = make_agent_md_output(workstation_list)
output_server = make_agent_md_output(server_list)
# Check if any agents were found
if not agent_list:
no_agents_message = f"<div class='alert alert-warning'>No agents found for client '{client_name}'"
if site_name:
no_agents_message += f" at site '{site_name}'"
no_agents_message += ".</div>"
documentation_doc.system_list = output
documentation_doc.workstation_list = output_workstation
documentation_doc.server_list = output_server
# 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):

View File

@ -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 = '''
<style>
/* Base card styles with more specific selectors */
div.card {
border: 1px solid #ddd !important;
border-radius: 4px !important;
padding: 15px !important;
margin-bottom: 15px !important;
background-color: #ffffff !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;
page-break-inside: avoid !important;
display: block !important;
width: 100% !important;
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
/* Header styles */
div.card h3 {
margin: 0 0 10px 0 !important;
color: #000000 !important;
font-size: 14px !important;
font-weight: bold !important;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
}
/* Metadata container */
div.card div.metadata {
margin: 10px 0 !important;
}
/* Individual metadata items */
div.card div.metadata-item {
margin-bottom: 5px !important;
font-size: 12px !important;
line-height: 1.4 !important;
display: flex !important;
flex-wrap: wrap !important;
}
/* Metadata labels */
div.card span.metadata-label {
color: #666666 !important;
font-weight: 600 !important;
min-width: 120px !important;
display: inline-block !important;
margin-right: 10px !important;
}
/* Status badges */
div.card span.status-badge {
display: inline-block !important;
padding: 2px 8px !important;
border-radius: 12px !important;
font-size: 11px !important;
font-weight: bold !important;
margin-left: 8px !important;
}
/* Horizontal rule */
div.card hr {
margin: 10px 0 !important;
border: none !important;
border-top: 1px solid #eee !important;
height: 1px !important;
}
/* Section headers */
div.section-break h3 {
margin: 20px 0 10px 0 !important;
color: #333333 !important;
font-size: 16px !important;
font-weight: bold !important;
border-bottom: 2px solid #eee !important;
padding-bottom: 5px !important;
}
@media print {
div.card {
border: 1px solid #000000 !important;
box-shadow: none !important;
}
}
</style>
'''
# 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 += '<div class="section-break" data-label="Systems from RMM">'
if server_items:
html += '<h3>Server Systems</h3>'
html += ''.join(render_single_card(item) for item in server_items)
if workstation_items:
html += '<h3>Workstation Systems</h3>'
html += ''.join(render_single_card(item) for item in workstation_items)
html += '</div>'
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'''
<div class="card">
<h3>{header}</h3>
<hr>
<div class="metadata">
<div class="metadata-item">
<span class="metadata-label">Location:</span>
<span>{location}</span>
</div>
'''
# Add all metadata fields if they exist
if 'metadata' in item:
for key, value in item['metadata'].items():
if value and key.lower() != 'ip' and key.lower() != 'local ips' and key.lower() != 'type': # Skip IP and Type as they're already in the header
html += f'''
<div class="metadata-item">
<span class="metadata-label">{key}:</span>
<span>{value}</span>
</div>
'''
# Add description if it exists
description = item.get('description', '') or item.get('documentation_text', '')
if description:
html += f'''
<div class="metadata-item">
<span class="metadata-label">Description:</span>
<span>{description}</span>
</div>
'''
html += '</div></div>'
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 "<p>No documented IT objects found in this landscape.</p>"
# 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")