148 lines
14 KiB
HTML
148 lines
14 KiB
HTML
{% extends 'base.html' %}{% block content %}
|
||
<div class="toolbar"><h1>{{t('mesh.title')}}</h1><div class="toolbar-actions"><button id="sync-dry-run" type="button" class="button button-secondary">{{ t('mesh.dry_run') }}</button><button id="sync-start" type="button">{{t('mesh.start')}}</button></div></div>
|
||
<div class="cards"><div class="card"><strong>{{linked}}</strong><span>{{t('mesh.linked_assets')}}</span></div><div class="card"><strong>{{conflicts}}</strong><span>{{t('mesh.conflicts')}}</span></div><div class="card"><strong>{{missing}}</strong><span>{{t('mesh.missing')}}</span></div></div>
|
||
|
||
<section class="panel"><div class="log-header"><h2>{{t('mesh.live_log')}}</h2><span id="sync-state" class="sync-status">{{t('mesh.ready')}}</span></div><div id="sync-log" class="sync-log"><div class="log-line log-info">{{t('mesh.ready_message')}}</div></div></section>
|
||
|
||
<section class="panel">
|
||
<h2>1. {{ t('mesh.connection') }}</h2>
|
||
<p class="muted">{{ t('mesh.password_env_help') }}</p>
|
||
<form method="post" action="/sync/meshcentral/settings" class="compact-form">
|
||
<div class="form-grid">
|
||
<label class="checkbox-label"><input class="checkbox" type="checkbox" name="enabled" {% if config.meshcentral.enabled %}checked{% endif %}> {{ t('common.active') }}</label>
|
||
<label>WebSocket-URL<input name="url" value="{{config.meshcentral.url or ''}}" placeholder="wss://meshcentral.example.org:443"></label>
|
||
<label>{{ t('mesh.username') }}<input name="username" value="{{config.meshcentral.username or ''}}"></label>
|
||
<label>{{ t('mesh.password_env') }}<input name="password_env" value="{{config.meshcentral.password_env or 'MESHCENTRAL_PASSWORD'}}"></label>
|
||
<label>Tenant<input name="tenant" value="{{config.meshcentral.tenant or ''}}"></label>
|
||
<label>meshctrl.js-Pfad<input name="meshctrl_path" value="{{config.meshcentral.meshctrl_path}}"></label>
|
||
<label>{{ t('mesh.timeout_seconds') }}<input type="number" min="10" max="3600" name="timeout_seconds" value="{{config.meshcentral.timeout_seconds or 600}}"></label>
|
||
</div>
|
||
<div class="checkbox-row">
|
||
<label class="checkbox-label"><input class="checkbox" type="checkbox" name="verify_tls" {% if config.meshcentral.verify_tls %}checked{% endif %}> {{ t('mesh.verify_tls') }}</label>
|
||
<label class="checkbox-label"><input class="checkbox" type="checkbox" name="include_details" {% if config.meshcentral.get('include_details',true) %}checked{% endif %}> {{ t('mesh.include_details') }}</label>
|
||
<label class="checkbox-label"><input class="checkbox" type="checkbox" name="create_missing_assets" {% if config.meshcentral.create_missing_assets %}checked{% endif %}> {{ t('mesh.create_missing_assets') }}</label>
|
||
<label class="checkbox-label"><input class="checkbox" type="checkbox" name="mark_missing_devices" {% if config.meshcentral.mark_missing_devices %}checked{% endif %}> {{ t('mesh.mark_missing_devices') }}</label>
|
||
<label class="checkbox-label"><input class="checkbox" type="checkbox" name="store_source_json" {% if config.meshcentral.get('store_source_json',true) %}checked{% endif %}> {{ t('mesh.store_source_json') }}</label>
|
||
</div>
|
||
<h3>{{ t('mesh.asset_matching') }}</h3>
|
||
<p class="muted">{{ t('mesh.asset_matching_help') }}</p>
|
||
<div class="checkbox-row">
|
||
<label class="checkbox-label"><input class="checkbox" type="checkbox" name="match_order" value="node_id" {% if 'node_id' in config.meshcentral.match_order %}checked{% endif %}> {{ t('mesh.match_node_id') }}</label>
|
||
<label class="checkbox-label"><input class="checkbox" type="checkbox" name="match_order" value="mac_address" {% if 'mac_address' in config.meshcentral.match_order %}checked{% endif %}> {{ t('mesh.match_mac') }}</label>
|
||
<label class="checkbox-label"><input class="checkbox" type="checkbox" name="match_order" value="serial_number" {% if 'serial_number' in config.meshcentral.match_order %}checked{% endif %}> {{ t('mesh.match_serial') }}</label>
|
||
<label class="checkbox-label"><input class="checkbox" type="checkbox" name="serial_match_manufacturer" {% if config.meshcentral.get('serial_match_manufacturer', false) %}checked{% endif %}> {{ t('mesh.match_serial_manufacturer') }}</label>
|
||
</div>
|
||
<p class="muted">{{ t('mesh.match_mac_help') }} {{ t('mesh.match_serial_manufacturer_help') }}</p>
|
||
<div class="form-grid mesh-identity-settings">
|
||
<label>{{ t('mesh.invalid_serial_values') }}
|
||
<textarea name="invalid_serial_values" rows="6" placeholder="Unknown N/A To be filled by O.E.M.">{{ (config.meshcentral.get('invalid_serial_values', []) or []) | join('\n') }}</textarea>
|
||
<small class="muted">{{ t('mesh.invalid_serial_values_help') }}</small>
|
||
</label>
|
||
<label>{{ t('mesh.manufacturer_invalid_serial_rules') }}
|
||
<textarea name="manufacturer_invalid_serial_rules" rows="6" placeholder="Synology | 123456789 Synology | Unknown">{% for rule in config.meshcentral.get('manufacturer_invalid_serial_rules', []) or [] %}{% for value in rule.get('values', []) or [] %}{{ rule.get('manufacturer', '') }} | {{ value }}{{ '\n' }}{% endfor %}{% endfor %}</textarea>
|
||
<small class="muted">{{ t('mesh.manufacturer_invalid_serial_rules_help') }}</small>
|
||
</label>
|
||
</div>
|
||
<div class="notice notice-info">{{ t('mesh.node_id_identity_help') }}</div>
|
||
<button class="button button-save">💾 {{ t('mesh.save_connection') }}</button>
|
||
</form>
|
||
</section>
|
||
|
||
<section class="panel">
|
||
<h2>2. {{ t('settings.mesh_mapping') }}</h2>
|
||
{% set mesh = config.meshcentral %}
|
||
{% set category_mapping = mesh.get('category_mapping', {}) %}
|
||
<p class="muted">{{ t('settings.mesh_mapping_help') }}</p>
|
||
<form method="post" action="/sync/meshcentral/categories/save" class="compact-form">
|
||
<label>{{ t('settings.fallback_category') }}
|
||
<select name="mesh_fallback_category_id" required>
|
||
{% for category in categories %}
|
||
<option value="{{ category.id }}" {% if category.id == (mesh.get('fallback_category_id', 1)|int) %}selected{% endif %}>{{ category.name }} (ID {{ category.id }})</option>
|
||
{% endfor %}
|
||
</select>
|
||
</label>
|
||
<div class="form-grid">
|
||
{% set mesh_types = [
|
||
('1', 'settings.mesh_type_desktop'),
|
||
('2', 'settings.mesh_type_notebook'),
|
||
('3', 'settings.mesh_type_mobile'),
|
||
('4', 'settings.mesh_type_server'),
|
||
('5', 'settings.mesh_type_5'),
|
||
('6', 'settings.mesh_type_6'),
|
||
('7', 'settings.mesh_type_7'),
|
||
('8', 'settings.mesh_type_linux_other')
|
||
] %}
|
||
{% for type_id, type_label in mesh_types %}
|
||
<label>{{ t(type_label) }} ({{ t('settings.meshcentral_type') }} {{ type_id }})
|
||
<select name="mesh_category_{{ type_id }}">
|
||
<option value="">{{ t('settings.use_fallback') }}</option>
|
||
{% for category in categories %}
|
||
<option value="{{ category.id }}" {% if category_mapping.get(type_id) is not none and category.id == (category_mapping.get(type_id)|int) %}selected{% endif %}>{{ category.name }} (ID {{ category.id }})</option>
|
||
{% endfor %}
|
||
</select>
|
||
</label>
|
||
{% endfor %}
|
||
</div>
|
||
<button class="button button-save">💾 {{ t('settings.save_mesh_mapping') }}</button>
|
||
</form>
|
||
</section>
|
||
|
||
<section class="panel">
|
||
<h2>3. {{ t('mesh.field_mappings') }}</h2>
|
||
<p class="muted">{{ t('mesh.mappings_help') }} {{ t('mesh.mappings_examples') }}: <code>node.osdesc</code>, <code>sys.hardware.windows.cpu[].Name</code>, <code>node.name|name</code>.</p>
|
||
<form method="post" action="/sync/meshcentral/mappings/save">
|
||
<div class="table-scroll management-table-scroll"><table class="data-table mesh-mapping-table"><thead><tr><th>{{ t('common.active') }}</th><th>{{ t('mesh.priority') }}</th><th>{{ t('mesh.target_field') }}</th><th>Source Path</th><th>{{ t('mesh.transformation') }}</th><th>{{ t('mesh.multi_values') }}</th><th>{{ t('mesh.separator') }}</th><th>{{ t('mesh.update_rule') }}</th><th>{{ t('common.description') }}</th><th></th></tr></thead><tbody>
|
||
{% for m in mappings %}
|
||
<tr>
|
||
<td><input type="checkbox" name="mapping_{{m.id}}_enabled" {% if m.enabled %}checked{% endif %}></td>
|
||
<td><input class="mapping-priority" type="number" name="mapping_{{m.id}}_priority" value="{{m.priority}}"></td>
|
||
<td data-filter-value="{% for d in definitions %}{% if d.field_name==m.target_field_name %}{{ d.label|e }} {{ d.field_name|e }} {{ 'System' if d.is_system else 'Custom' }}{% endif %}{% endfor %}"><select name="mapping_{{m.id}}_target_field_name">{% for d in definitions %}<option value="{{d.field_name}}" {% if d.field_name==m.target_field_name %}selected{% endif %}>{{d.label}} ({{d.field_name}}) – {{'System' if d.is_system else 'Custom'}}</option>{% endfor %}</select>{% if m.is_system %}<small class="muted">{{ t('mesh.system_mapping') }}</small>{% else %}<small class="muted">{{ t('mesh.custom_mapping') }}</small>{% endif %}</td>
|
||
<td><input class="mapping-path" name="mapping_{{m.id}}_source_path" value="{{m.source_path}}" required></td>
|
||
<td data-filter-value="{{ m.transform|e }}"><select name="mapping_{{m.id}}_transform">{% for v in transform_options %}<option value="{{v}}" {% if m.transform==v %}selected{% endif %}>{{v}}</option>{% endfor %}</select></td>
|
||
<td><select name="mapping_{{m.id}}_multi_value_mode">{% for v in ['first','join','json','count'] %}<option value="{{v}}" {% if m.multi_value_mode==v %}selected{% endif %}>{{v}}</option>{% endfor %}</select></td>
|
||
<td><input class="mapping-separator" name="mapping_{{m.id}}_separator" value="{{m.separator|replace('\n','\\n')|replace('\t','\\t')}}"></td>
|
||
<td><select name="mapping_{{m.id}}_update_rule"><option value="fill_empty" {% if m.update_rule=='fill_empty' %}selected{% endif %}>{{ t('mesh.rule_fill_empty') }}</option><option value="meshcentral_wins" {% if m.update_rule=='meshcentral_wins' %}selected{% endif %}>{{ t('mesh.rule_mesh_wins') }}</option><option value="local_wins" {% if m.update_rule=='local_wins' %}selected{% endif %}>{{ t('mesh.rule_local_wins') }}</option></select></td>
|
||
<td><input name="mapping_{{m.id}}_description" value="{{m.description or ''}}"></td>
|
||
<td>{% if not m.is_system %}<button class="button danger" type="submit" formaction="/sync/meshcentral/mappings/{{m.id}}/delete" formmethod="post" onclick="return confirm({{ t('mesh.delete_mapping_confirm')|tojson }})">{{ t('common.delete') }}</button>{% endif %}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody></table></div>
|
||
<button class="button button-save">💾 {{ t('mesh.save_mappings') }}</button>
|
||
</form>
|
||
</section>
|
||
|
||
<section class="panel">
|
||
<h2>4. {{ t('mesh.new_mapping') }}</h2>
|
||
<p class="muted">{{ t('mesh.new_mapping_help') }}</p>
|
||
<form method="post" action="/sync/meshcentral/mappings/new" class="compact-form">
|
||
<div class="form-grid">
|
||
<label>Zielfeld<select name="target_field_name" required><option value="">{{ t('common.select') }}</option>{% for d in definitions %}<option value="{{d.field_name}}">{{d.label}} ({{d.field_name}}) – {{'System' if d.is_system else 'Custom'}}</option>{% endfor %}</select></label>
|
||
<label>Source Path<input name="source_path" required placeholder="node.example.path"></label>
|
||
<label>Transformation<select name="transform">{% for v in transform_options %}<option value="{{v}}">{{v}}</option>{% endfor %}</select></label>
|
||
<label>Mehrfachwerte<select name="multi_value_mode"><option value="first">first</option><option value="join">join</option><option value="json">json</option><option value="count">count</option></select></label>
|
||
<label>Trennzeichen<input name="separator" value="\n"></label>
|
||
<label>Update-Regel<select name="update_rule"><option value="fill_empty">{{ t('mesh.rule_fill_empty') }}</option><option value="meshcentral_wins">{{ t('mesh.rule_mesh_wins') }}</option><option value="local_wins">{{ t('mesh.rule_local_wins') }}</option></select></label>
|
||
<label>Beschreibung<input name="description"></label>
|
||
<label class="checkbox-label"><input class="checkbox" type="checkbox" name="enabled" checked> {{ t('common.active') }}</label>
|
||
</div><button class="button button-save">+ {{ t('mesh.create_mapping') }}</button>
|
||
</form>
|
||
</section>
|
||
|
||
<section class="panel"><h2>{{t('mesh.recent_runs')}}</h2><table><thead><tr><th>{{t('mesh.start_time')}}</th><th>{{t('assets.status')}}</th><th>{{t('mesh.found')}}</th><th>{{t('mesh.created')}}</th><th>{{t('mesh.changed')}}</th><th>{{t('mesh.conflicts')}}</th><th>{{t('mesh.errors')}}</th><th>{{t('mesh.message')}}</th></tr></thead><tbody>{% for run in runs %}<tr><td><time class="local-time" datetime="{{ utc_iso(run.started_at) }}" data-utc="{{ utc_iso(run.started_at) }}">{{ run.started_at }}</time></td><td>{{run.status}}</td><td>{{run.devices_found}}</td><td>{{run.created_count}}</td><td>{{run.updated_count}}</td><td>{{run.conflict_count}}</td><td>{{run.error_count}}</td><td>{{run.message or ''}}</td></tr>{% else %}<tr><td colspan="8">{{t('mesh.no_runs')}}</td></tr>{% endfor %}</tbody></table></section>
|
||
<script>(()=>{
|
||
const startButton=document.getElementById('sync-start');
|
||
const dryButton=document.getElementById('sync-dry-run');
|
||
const buttons=[startButton,dryButton].filter(Boolean);
|
||
const logBox=document.getElementById('sync-log');
|
||
const state=document.getElementById('sync-state');
|
||
let offset=0,timer=null;
|
||
function setBusy(busy){buttons.forEach(button=>button.disabled=busy)}
|
||
function addLog(e){const l=document.createElement('div');l.className=`log-line log-${e.level||'info'}`;const localTime=window.AssetManagerTime?.formatUtc(e.time,e.time||'--')||(e.time||'--');l.textContent=`[${localTime}] ${e.message}`;logBox.appendChild(l);logBox.scrollTop=logBox.scrollHeight}
|
||
async function jsonResponse(response,fallback){const text=await response.text();let data;try{data=JSON.parse(text)}catch(_){throw new Error(`${fallback} (HTTP ${response.status}): ${text.slice(0,300)||'{{ t('mesh.empty_response') }}'}`)}if(!response.ok)throw new Error(data.error||data.detail||fallback);return data}
|
||
async function poll(id){try{const r=await fetch(`/sync/meshcentral/jobs/${id}?offset=${offset}`,{cache:'no-store'}),d=await jsonResponse(r,'{{ t('mesh.log_load_error') }}');d.logs.forEach(addLog);offset=d.next_offset;state.textContent=d.status;if(d.finished){clearInterval(timer);setBusy(false)}}catch(e){clearInterval(timer);setBusy(false);addLog({level:'error',message:e.message})}}
|
||
async function startJob(url){setBusy(true);logBox.innerHTML='';offset=0;try{const r=await fetch(url,{method:'POST'}),d=await jsonResponse(r,'{{ t('mesh.start_error') }}');await poll(d.job_id);timer=setInterval(()=>poll(d.job_id),800)}catch(e){addLog({level:'error',message:e.message});setBusy(false)}}
|
||
startButton.onclick=()=>startJob('/sync/meshcentral/start');
|
||
if(dryButton)dryButton.onclick=()=>startJob('/sync/meshcentral/dry-run');
|
||
})();</script>
|
||
{% endblock %}
|