Files
Assetmanager/app/templates/job_definition_form.html
T

183 lines
23 KiB
HTML

{% extends 'base.html' %}
{% block content %}
<div class="toolbar"><div><h1>{{ t('jobdefs.edit') if definition else t('jobdefs.new') }}</h1><p class="muted">{{ t('jobdefs.admin_only') }}</p></div><a class="button button-secondary" href="/job-definitions">{{ t('common.back') }}</a></div>
<form method="post" action="{{ '/job-definitions/' ~ definition.id if definition else '/job-definitions' }}" class="panel form-grid job-definition-form" id="job-definition-form">
<label class="checkbox-label job-enabled-top form-wide"><input type="checkbox" name="enabled" {% if not definition or definition.enabled %}checked{% endif %}> {{ t('common.active') }}</label>
<label>{{ t('common.name') }}<input name="name" required maxlength="180" value="{{ definition.name if definition else '' }}"></label>
<label>{{ t('jobdefs.kind') }}<select name="job_kind" id="job-kind"><option value="script" {% if not definition or definition.job_kind=='script' %}selected{% endif %}>{{ t('jobdefs.kind.script') }}</option><option value="registry" {% if definition and definition.job_kind=='registry' %}selected{% endif %}>{{ t('jobdefs.kind.registry') }}</option></select><small>{{ t('jobdefs.kind.help') }}</small></label>
<label>{{ t('jobdefs.platform') }}<select name="platform">{% for x in ['windows','linux','macos','all'] %}<option value="{{x}}" {% if definition and definition.platform==x %}selected{% endif %}>{{ t('jobdefs.platform.'~x,x) }}</option>{% endfor %}</select></label>
<label class="script-only-field">{{ t('jobdefs.interpreter') }}<select name="interpreter" id="job-interpreter">{% for x in ['powershell','cmd','bash','python'] %}<option value="{{x}}" {% if definition and definition.interpreter==x %}selected{% endif %}>{{ t('jobdefs.interpreter.'~x,x) }}</option>{% endfor %}</select></label>
<label class="script-only-field">{{ t('jobdefs.source') }}<select name="source_type" id="job-source-type">{% for x in ['inline','mounted','unc'] %}<option value="{{x}}" {% if definition and definition.source_type==x %}selected{% endif %}>{{ t('jobdefs.source.'~x,x) }}</option>{% endfor %}</select></label>
<label class="form-wide">{{ t('common.description') }}<textarea name="description" rows="2">{{ definition.description if definition else '' }}</textarea></label>
<label class="form-wide job-source-path">{{ t('jobdefs.path') }}<input name="source_path" value="{{ definition.source_path if definition else '' }}" placeholder="/scripts/windows/example.ps1 oder \\server\share\script.ps1"><small>{{ t('jobdefs.path_help') }}</small></label>
<div class="form-wide job-inline-script">
<label>{{ t('jobdefs.own_script') }}<textarea id="job-inline-script" name="inline_script" rows="18" spellcheck="false" class="code-editor">{{ definition.inline_script if definition else '' }}</textarea><small>{{ t('jobdefs.own_script_help') }}</small></label>
<div class="form-actions" style="margin-top:.75rem"><button class="button button-secondary" type="button" id="insert-job-test-script">{{ t('jobdefs.insert_test_commands') }}</button></div>
<section class="job-script-help" aria-live="polite">
<h3>{{ t('jobdefs.script_help.title') }}</h3>
<p>{{ t('jobdefs.script_help.common') }}</p>
<div class="job-script-help-common">
<p><strong>{{ t('jobdefs.script_help.variables') }}</strong></p>
{% raw %}<code>{{JobId}}</code>, <code>{{AssetName}}</code>, <code>{{Hostname}}</code>, <code>{{IPAddress}}</code>, <code>{{CurrentUser}}</code>, <code>{{SerialNumber}}</code>, <code>{{Manufacturer}}</code>, <code>{{Model}}</code>, <code>{{Department}}</code>, <code>{{Location}}</code>, <code>{{MeshNodeId}}</code>{% endraw %}
<p><strong>{{ t('jobdefs.script_help.parameters') }}</strong> {% raw %}<code>{{Param.ParameterKey}}</code>{% endraw %}</p>
</div>
<div data-script-help="powershell">
<p>{{ t('jobdefs.script_help.powershell') }}</p>
<pre class="sync-log">$count = 3
Write-JobLog "Gefundene Dateien: $count"
$JobResult["processed_count"] = $count</pre>
</div>
<div data-script-help="python">
<p>{{ t('jobdefs.script_help.python') }}</p>
<pre class="sync-log">count = 3
print(f"Processed files: {count}")
job_result["processed_count"] = count</pre>
</div>
<div data-script-help="bash">
<p>{{ t('jobdefs.script_help.bash') }}</p>
<pre class="sync-log">count=3
echo "Processed files: $count"
{% raw %}echo "Asset: {{Hostname}}"{% endraw %}</pre>
</div>
<div data-script-help="cmd">
<p>{{ t('jobdefs.script_help.cmd') }}</p>
<pre class="sync-log">set "COUNT=3"
echo Processed files: %COUNT%
{% raw %}echo Asset: {{Hostname}}{% endraw %}</pre>
</div>
<p class="muted" id="job-source-helper-note">{{ t('jobdefs.script_help.wrapper_sources') }}</p>
</section>
</div>
<section class="form-wide job-registry-editor" hidden>
<div class="toolbar"><div><h2>{{ t('registry.title') }}</h2><p class="muted">{{ t('registry.help') }}</p></div><button type="button" class="button button-secondary" id="add-registry-entry">{{ t('registry.add') }}</button></div>
<label class="checkbox-label"><input type="checkbox" name="registry_backup" {% if definition and definition.configuration and definition.configuration.get('registry_backup') %}checked{% endif %}> {{ t('registry.backup') }}</label>
<div class="registry-parameter-help"><strong>{{ t('registry.parameters.title') }}</strong><p>{{ t('registry.parameters.body') }}</p><code>{% raw %}{{Param.TechnicalKey}}{% endraw %}</code><div id="registry-parameter-keys" class="muted"></div></div>
<input type="hidden" name="registry_entries_json" id="registry-entries-json" value="[]">
<datalist id="registry-rights-list"><option value="ReadKey"><option value="SetValue"><option value="CreateSubKey"><option value="WriteKey"><option value="ChangePermissions"><option value="FullControl"></datalist>
<div id="registry-entry-rows"></div>
<details class="job-script-help"><summary>{{ t('registry.notes.title') }}</summary><p>{{ t('registry.notes.body') }}</p><pre class="sync-log">HKLM\SOFTWARE\Example
Name: Enabled
Type: REG_DWORD
{% raw %}Value: {{Param.Enabled}}{% endraw %}</pre></details>
</section>
<section class="form-wide job-parameters-editor"><div class="toolbar"><div><h2>{{ t('jobparams.title') }}</h2><p class="muted">{{ t('jobparams.help') }}</p></div><button type="button" class="button button-secondary" id="add-job-parameter">{{ t('jobparams.add') }}</button></div>
<input type="hidden" name="parameters_json" id="job-parameters-json" value="[]">
<div id="job-parameter-rows"></div></section>
<label class="script-only-field">{{ t('jobdefs.arguments') }}<input name="arguments" value="{{ definition.arguments if definition else '' }}"></label>
<label>{{ t('jobdefs.timeout') }}<input type="number" name="timeout_seconds" min="1" max="86400" value="{{ definition.timeout_seconds if definition else 300 }}"></label>
<label class="script-only-field">{{ t('jobdefs.success_codes') }}<input name="success_codes" value="{{ definition.success_codes if definition else '0' }}"></label>
<div class="form-actions form-wide"><button class="button button-save" type="submit">{{ t('common.save') }}</button>{% if definition %}<button class="button button-secondary" type="submit" formaction="/job-definitions/{{ definition.id }}/test">{{ t('jobdefs.test') }}</button>{% endif %}</div>
</form>
{% if test_result %}<section class="panel"><h2>{{ t('jobdefs.test_result') }}</h2><pre class="sync-log">{{ test_result }}</pre></section>{% endif %}
{% if script_preview is not none %}<section class="panel"><div class="toolbar"><div><h2>{{ t('jobdefs.preview') }}</h2><p class="muted">{{ t('jobdefs.preview_help') }}</p></div><button class="button button-secondary" type="button" id="copy-job-preview">{{ t('jobdefs.copy_preview') }}</button></div><pre class="sync-log" id="job-script-preview">{{ script_preview }}</pre>{% if preview_values %}<details><summary>{{ t('jobdefs.preview.sample_notice') }}</summary><pre class="sync-log">{% for key,value in preview_values.items() %}{{ key }} = {{ value }}
{% endfor %}</pre></details>{% endif %}</section>{% endif %}
<style>
.registry-entry-row{border:1px solid var(--border-color,#415166);border-radius:8px;padding:1rem;margin-top:1rem}.registry-entry-row [hidden]{display:none!important}.registry-mode-picker{border:0;padding:0;margin:0}.registry-mode-options{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:.6rem}.registry-mode-options label{display:flex;align-items:flex-start;gap:.55rem;border:1px solid var(--border-color,#415166);border-radius:7px;padding:.75rem;cursor:pointer}.registry-mode-options label:has(input:checked){outline:2px solid var(--accent-color,#20a5df);background:rgba(32,165,223,.08)}.registry-mode-options input{margin-top:.2rem}.registry-mode-options span{display:flex;flex-direction:column;gap:.2rem}.registry-mode-options small{opacity:.75}.registry-set-fields,.registry-permission-fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.75rem}.registry-checkbox-stack{display:flex;flex-direction:column;align-items:stretch;gap:.55rem;grid-column:1/-1;width:100%}.registry-checkbox-stack .checkbox-label,.job-definition-form .checkbox-label{display:flex!important;flex-direction:row!important;align-items:center!important;justify-content:flex-start!important;gap:.55rem!important;width:100%!important;max-width:none!important;text-align:left!important}.registry-checkbox-stack .checkbox-label input,.job-definition-form .checkbox-label input{flex:0 0 auto!important;margin:0!important}.registry-parameter-help{border-left:3px solid var(--accent-color,#20a5df);padding:.65rem .85rem;margin:.75rem 0}.registry-parameter-help p{margin:.25rem 0}.script-only-field[hidden]{display:none!important}.registry-delete-value-fields{display:grid;grid-template-columns:minmax(0,1fr);gap:.75rem}@media(max-width:800px){.registry-set-fields,.registry-permission-fields{grid-template-columns:1fr}}
</style>
<script>
(() => {
const kind=document.getElementById('job-kind'), source=document.getElementById('job-source-type'), interpreter=document.getElementById('job-interpreter'), inlineArea=document.querySelector('.job-inline-script'), pathArea=document.querySelector('.job-source-path'), registryArea=document.querySelector('.job-registry-editor');
const sync=()=>{const isRegistry=kind?.value==='registry';const x=!isRegistry&&source?.value==='inline';if(inlineArea)inlineArea.style.display=x?'block':'none';if(pathArea)pathArea.style.display=(!isRegistry&&!x)?'flex':'none';if(registryArea)registryArea.hidden=!isRegistry;document.querySelectorAll('.script-only-field').forEach(el=>{el.hidden=isRegistry;el.querySelectorAll('input,select,textarea').forEach(c=>c.disabled=isRegistry)});document.querySelectorAll('[data-script-help]').forEach(el=>el.hidden=el.dataset.scriptHelp!==interpreter?.value)}; kind?.addEventListener('change',sync);source?.addEventListener('change',sync);interpreter?.addEventListener('change',sync);sync();
const initial=[{% if definition %}{% for x in definition.parameters %}{{ {'parameter_key':x.parameter_key,'label':x.label,'description':x.description or '','data_type':x.data_type,'default_value':x.default_value or '','required':x.required}|tojson }}{% if not loop.last %},{% endif %}{% endfor %}{% endif %}];
const rows=document.getElementById('job-parameter-rows'), hidden=document.getElementById('job-parameters-json');
const esc=v=>String(v??'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
function addRow(p={}){const d=document.createElement('div');d.className='job-parameter-row';d.innerHTML=`<label>{{ t('jobparams.key') }}<input data-p="parameter_key" value="${esc(p.parameter_key)}" pattern="[A-Za-z0-9_]+"></label><label>{{ t('jobparams.label') }}<input data-p="label" value="${esc(p.label)}"></label><label>{{ t('jobparams.type') }}<select data-p="data_type">${['text','number','boolean','path'].map(x=>`<option value="${x}" ${p.data_type===x?'selected':''}>${x}</option>`).join('')}</select></label><label>{{ t('jobparams.default') }}<input data-p="default_value" value="${esc(p.default_value)}"></label><label class="form-wide">{{ t('common.description') }}<textarea data-p="description" rows="2">${esc(p.description)}</textarea></label><label class="checkbox-label"><input type="checkbox" data-p="required" ${p.required?'checked':''}> {{ t('jobparams.required') }}</label><button type="button" class="button button-danger" data-remove>{{ t('common.delete') }}</button>`;d.querySelector('[data-remove]').onclick=()=>d.remove();rows.appendChild(d)}
const updateRegistryParameterKeys=()=>{const target=document.getElementById('registry-parameter-keys');if(!target)return;const keys=[...rows.querySelectorAll('[data-p="parameter_key"]')].map(x=>x.value.trim()).filter(Boolean);target.textContent=keys.length?{{ t('registry.parameters.available')|tojson }}+' '+keys.map(k=>String.fromCharCode(123,123)+'Param.'+k+String.fromCharCode(125,125)).join(', '):{{ t('registry.parameters.none')|tojson }}};
initial.forEach(addRow); document.getElementById('add-job-parameter')?.addEventListener('click',()=>{addRow({data_type:'text'});updateRegistryParameterKeys()}); rows.addEventListener('input',updateRegistryParameterKeys); rows.addEventListener('click',()=>setTimeout(updateRegistryParameterKeys)); updateRegistryParameterKeys();
document.getElementById('insert-job-test-script')?.addEventListener('click',()=>{const e=document.getElementById('job-inline-script');if(e)e.value={{ t('jobdefs.test_commands_content')|tojson }}});
const registryInitial={{ (definition.configuration.get('registry_entries', []) if definition and definition.configuration else [])|tojson }};
const registryRows=document.getElementById('registry-entry-rows'), registryHidden=document.getElementById('registry-entries-json');
function addRegistryRow(p={}){
const d=document.createElement('div');
d.className='job-parameter-row registry-entry-row';
const legacyPermission=p.action==='set_permissions';
let mode='set';
if(legacyPermission || (p.action==='set' && p.set_value===false && p.permissions_enabled)) mode='permissions';
else if(p.action==='delete_value') mode='delete_value';
else if(p.action==='delete_key') mode='delete_key';
const knownPrincipals={'S-1-5-32-545':'builtin_users','S-1-5-32-544':'builtin_admins','S-1-5-18':'system','S-1-5-11':'authenticated_users','S-1-1-0':'everyone'};
const principalValue=p.principal||'S-1-5-32-545';
const principalChoice=knownPrincipals[principalValue]||'custom';
const knownRights=['ReadKey','WriteKey','FullControl'];
const rightsValue=p.rights||'FullControl';
const rightsChoice=knownRights.includes(rightsValue)?rightsValue:'custom';
const id='registry-mode-'+Math.random().toString(36).slice(2);
d.innerHTML=`
<fieldset class="registry-mode-picker form-wide">
<legend>{{ t('registry.operation') }}</legend>
<div class="registry-mode-options">
<label><input type="radio" name="${id}" value="set" ${mode==='set'?'checked':''}><span><strong>{{ t('registry.action.set') }}</strong><small>{{ t('registry.action.set.help') }}</small></span></label>
<label><input type="radio" name="${id}" value="permissions" ${mode==='permissions'?'checked':''}><span><strong>{{ t('registry.action.set_permissions') }}</strong><small>{{ t('registry.action.set_permissions.help') }}</small></span></label>
<label><input type="radio" name="${id}" value="delete_value" ${mode==='delete_value'?'checked':''}><span><strong>{{ t('registry.action.delete_value') }}</strong><small>{{ t('registry.action.delete_value.help') }}</small></span></label>
<label><input type="radio" name="${id}" value="delete_key" ${mode==='delete_key'?'checked':''}><span><strong>{{ t('registry.action.delete_key') }}</strong><small>{{ t('registry.action.delete_key.help') }}</small></span></label>
</div>
</fieldset>
<input type="hidden" data-r="action" value="${esc(p.action||'set')}">
<input type="hidden" data-r="set_value" value="${p.set_value!==false?'true':'false'}">
<input type="hidden" data-r="permissions_enabled" value="${p.permissions_enabled?'true':'false'}">
<label>{{ t('registry.hive') }}<select data-r="hive">${['HKLM','HKCU','HKCR','HKU','HKCC'].map(x=>`<option value="${x}" ${p.hive===x?'selected':''}>${x}</option>`).join('')}</select></label>
<label class="form-wide">{{ t('registry.path') }}<input data-r="path" required value="${esc(p.path)}" placeholder="SOFTWARE\\Vendor\\Product oder {% raw %}{{Param.RegistryPath}}{% endraw %}"><small>{{ t('registry.path.help') }}</small></label>
<div class="registry-set-fields form-wide">
<label>{{ t('registry.name') }}<input data-r="name" value="${esc(p.name)}"></label>
<label>{{ t('registry.type') }}<select data-r="value_type">${['REG_SZ','REG_EXPAND_SZ','REG_MULTI_SZ','REG_DWORD','REG_QWORD','REG_BINARY'].map(x=>`<option value="${x}" ${p.value_type===x?'selected':''}>${x}</option>`).join('')}</select></label>
<label class="form-wide">{{ t('registry.value') }}<textarea data-r="value" rows="2" placeholder="{% raw %}{{Param.Value}}{% endraw %}">${esc(p.value)}</textarea></label>
<div class="registry-checkbox-stack"><label class="checkbox-label"><input type="checkbox" data-r="only_if_missing" ${p.only_if_missing?'checked':''}> {{ t('registry.only_if_missing') }}</label></div>
</div>
<div class="registry-delete-value-fields form-wide">
<label>{{ t('registry.name') }}<input data-r-display="delete-name" value="${esc(p.name)}"></label>
</div>
<div class="registry-permission-fields form-wide">
<label>{{ t('registry.principal') }}
<select data-principal-choice>
<option value="builtin_users" ${principalChoice==='builtin_users'?'selected':''}>{{ t('registry.principal.users') }} [S-1-5-32-545]</option>
<option value="builtin_admins" ${principalChoice==='builtin_admins'?'selected':''}>{{ t('registry.principal.admins') }} [S-1-5-32-544]</option>
<option value="system" ${principalChoice==='system'?'selected':''}>SYSTEM [S-1-5-18]</option>
<option value="authenticated_users" ${principalChoice==='authenticated_users'?'selected':''}>{{ t('registry.principal.authenticated') }} [S-1-5-11]</option>
<option value="everyone" ${principalChoice==='everyone'?'selected':''}>{{ t('registry.principal.everyone') }} [S-1-1-0]</option>
<option value="custom" ${principalChoice==='custom'?'selected':''}>{{ t('registry.custom') }}</option>
</select>
</label>
<label class="registry-custom-principal">{{ t('registry.principal_custom') }}<input data-principal-custom value="${esc(principalChoice==='custom'?principalValue:'')}"><small>{{ t('registry.principal_help') }}</small></label>
<input type="hidden" data-r="principal" value="${esc(principalValue)}">
<label>{{ t('registry.rights') }}
<select data-rights-choice>
<option value="ReadKey" ${rightsChoice==='ReadKey'?'selected':''}>{{ t('registry.rights.read') }}</option>
<option value="WriteKey" ${rightsChoice==='WriteKey'?'selected':''}>{{ t('registry.rights.change') }}</option>
<option value="FullControl" ${rightsChoice==='FullControl'?'selected':''}>{{ t('registry.rights.full') }}</option>
<option value="custom" ${rightsChoice==='custom'?'selected':''}>{{ t('registry.custom') }}</option>
</select>
</label>
<label class="registry-custom-rights">{{ t('registry.rights_custom') }}<input data-rights-custom list="registry-rights-list" value="${esc(rightsChoice==='custom'?rightsValue:'')}"></label>
<input type="hidden" data-r="rights" value="${esc(rightsValue)}">
<label>{{ t('registry.access_type') }}<select data-r="access_type"><option value="Allow" ${p.access_type!=='Deny'?'selected':''}>Allow</option><option value="Deny" ${p.access_type==='Deny'?'selected':''}>Deny</option></select></label>
<div class="registry-checkbox-stack">
<label class="checkbox-label"><input type="checkbox" data-r="inherit_subkeys" ${p.inherit_subkeys!==false?'checked':''}> {{ t('registry.inherit_subkeys') }}</label>
<label class="checkbox-label"><input type="checkbox" data-r="replace_existing" ${p.replace_existing!==false?'checked':''}> {{ t('registry.replace_existing') }}</label>
</div>
</div>
<button type="button" class="button button-danger" data-remove>{{ t('common.delete') }}</button>`;
const modeInputs=[...d.querySelectorAll(`input[name="${id}"]`)];
const actionField=d.querySelector('[data-r="action"]');
const setValueField=d.querySelector('[data-r="set_value"]');
const permissionsField=d.querySelector('[data-r="permissions_enabled"]');
const valueName=d.querySelector('[data-r="name"]');
const deleteName=d.querySelector('[data-r-display="delete-name"]');
const principalMap={builtin_users:'S-1-5-32-545',builtin_admins:'S-1-5-32-544',system:'S-1-5-18',authenticated_users:'S-1-5-11',everyone:'S-1-1-0'};
const syncPrincipal=()=>{const c=d.querySelector('[data-principal-choice]').value;const custom=d.querySelector('[data-principal-custom]');d.querySelector('.registry-custom-principal').hidden=c!=='custom';d.querySelector('[data-r="principal"]').value=c==='custom'?custom.value:(principalMap[c]||'')};
const syncRights=()=>{const c=d.querySelector('[data-rights-choice]').value;const custom=d.querySelector('[data-rights-custom]');d.querySelector('.registry-custom-rights').hidden=c!=='custom';d.querySelector('[data-r="rights"]').value=c==='custom'?custom.value:c};
const syncRegistryFields=()=>{const m=modeInputs.find(x=>x.checked)?.value||'set';actionField.value=(m==='set'||m==='permissions')?'set':m;setValueField.value=m==='set'?'true':'false';permissionsField.value=m==='permissions'?'true':'false';const sections={set:d.querySelector('.registry-set-fields'),delete_value:d.querySelector('.registry-delete-value-fields'),permissions:d.querySelector('.registry-permission-fields')};Object.entries(sections).forEach(([key,section])=>{const active=m===key;section.hidden=!active;section.setAttribute('aria-hidden',active?'false':'true');section.querySelectorAll('input,select,textarea').forEach(control=>control.disabled=!active)});if(m==='delete_value') deleteName.value=valueName.value; else if(deleteName.value) valueName.value=deleteName.value};
modeInputs.forEach(x=>x.addEventListener('change',syncRegistryFields));
deleteName.addEventListener('input',()=>valueName.value=deleteName.value);
d.querySelector('[data-principal-choice]').addEventListener('change',syncPrincipal);d.querySelector('[data-principal-custom]').addEventListener('input',syncPrincipal);
d.querySelector('[data-rights-choice]').addEventListener('change',syncRights);d.querySelector('[data-rights-custom]').addEventListener('input',syncRights);
syncPrincipal();syncRights();syncRegistryFields();
d.querySelector('[data-remove]').onclick=()=>d.remove();registryRows.appendChild(d)
}
registryInitial.forEach(addRegistryRow);document.getElementById('add-registry-entry')?.addEventListener('click',()=>addRegistryRow({action:'set',hive:'HKLM',value_type:'REG_SZ'}));
document.getElementById('job-definition-form')?.addEventListener('submit',()=>{hidden.value=JSON.stringify([...rows.children].map(r=>{const o={};r.querySelectorAll('[data-p]').forEach(x=>o[x.dataset.p]=x.type==='checkbox'?x.checked:x.value);return o}));registryHidden.value=JSON.stringify([...registryRows.children].map(r=>{const o={};r.querySelectorAll('[data-r]').forEach(x=>{let v=x.type==='checkbox'?x.checked:x.value;if((x.dataset.r==='set_value'||x.dataset.r==='permissions_enabled')&&typeof v==='string')v=v==='true';o[x.dataset.r]=v});return o}))});
document.getElementById('copy-job-preview')?.addEventListener('click',async()=>{await navigator.clipboard.writeText(document.getElementById('job-script-preview')?.textContent||'')});
})();
</script>
{% endblock %}