sshkeyman 0.1.1

Web-based SSH key & config manager in Rust.
<!DOCTYPE html>
<html lang="{{ t["lang"] }}">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ t["edit_title"] }}</title>
    <link rel="stylesheet" href="/static/style.css">
</head>
<body>
    <div class="header">
        <h1>{{ t["nav_title"] }}</h1>
        <nav>
            <a href="/" class="nav-link">{{ t["nav_keys"] }}</a>
            <a href="/config" class="nav-link active">{{ t["nav_config"] }}</a>
            <a href="/config/raw" class="nav-link">{{ t["nav_raw_edit"] }}</a>
            <a href="/backup" class="nav-link">{{ t["nav_backup_all"] }}</a>
        </nav>
    </div>

    <div class="main" style="max-width:700px;margin:0 auto;">
        <a href="/config" style="color:#1565c0;text-decoration:none;">{{ t["edit_back"] }}</a>

        <h2 style="margin:16px 0;">{% if is_new %}{{ t["edit_add_title"] }}{% else %}{{ t["edit_edit_title_prefix"] }}{{ host_pattern }}{% endif %}</h2>

        <form method="post" action="/config/save">
            <input type="hidden" name="original_host" value="{{ host_pattern }}">

            <div class="form-group">
                <label for="host_pattern" class="field-label">{{ t["edit_host_pattern_label"] }}</label>
                <input type="text" id="host_pattern" name="host_pattern" value="{{ host_pattern }}"
                       placeholder="{{ t["edit_host_pattern_placeholder"] }}" required
                       style="width:100%;padding:8px 12px;border:1px solid #ccc;border-radius:4px;font-size:14px;">
            </div>

            <h3 style="margin:20px 0 10px;">{{ t["edit_fields_heading"] }}</h3>
            <div id="fields-container"></div>

            <button type="button" class="btn btn-secondary" onclick="addField()" style="margin:10px 0;">
                {{ t["edit_add_field"] }}
            </button>

            <div class="form-actions" style="margin-top:20px;">
                <button type="submit" class="btn btn-primary">{{ t["edit_save"] }}</button>
                <a href="/config" class="btn btn-secondary">{{ t["edit_cancel"] }}</a>
            </div>
        </form>
    </div>

    <script>
        const L = {{ js_locale_json|safe }};
        const fields = {{ fields_json|safe }};
        const availableKeys = {{ available_keys_json|safe }};
        const container = document.getElementById('fields-container');

        const commonFields = [
            'HostName', 'User', 'Port', 'IdentityFile', 'ProxyJump',
            'ProxyCommand', 'ForwardAgent', 'ServerAliveInterval',
            'StrictHostKeyChecking', 'AddKeysToAgent', 'IdentitiesOnly',
            'LocalForward', 'RemoteForward', 'DynamicForward',
            'Compression', 'ConnectTimeout', 'RequestTTY'
        ];

        function addField(key, value) {
            key = key || '';
            value = value || '';
            const row = document.createElement('div');
            row.className = 'field-row';
            row.style.cssText = 'display:flex;gap:8px;margin-bottom:8px;align-items:center;';

            const keyInput = document.createElement('input');
            keyInput.type = 'text';
            keyInput.name = 'field_keys';
            keyInput.value = key;
            keyInput.placeholder = L.key_placeholder;
            keyInput.setAttribute('list', 'field-keys-list');
            keyInput.style.cssText = 'flex:1;padding:8px;border:1px solid #ccc;border-radius:4px;font-size:14px;';
            keyInput.addEventListener('change', function() {
                swapValueInput(row, this.value, '');
            });

            const valInput = createValueInput(key, value);

            const removeBtn = document.createElement('button');
            removeBtn.type = 'button';
            removeBtn.textContent = L.remove;
            removeBtn.className = 'btn btn-danger btn-sm';
            removeBtn.onclick = () => row.remove();

            row.appendChild(keyInput);
            row.appendChild(valInput);
            row.appendChild(removeBtn);
            container.appendChild(row);
        }

        function createValueInput(key, value) {
            if (key.toLowerCase() === 'identityfile' && availableKeys.length > 0) {
                const sel = document.createElement('select');
                sel.name = 'field_values';
                sel.style.cssText = 'flex:2;padding:8px;border:1px solid #ccc;border-radius:4px;font-size:14px;';

                const blank = document.createElement('option');
                blank.value = '';
                blank.textContent = L.select_key;
                sel.appendChild(blank);

                availableKeys.forEach(function(k) {
                    const opt = document.createElement('option');
                    opt.value = '~/.ssh/' + k;
                    opt.textContent = k;
                    if (value === opt.value || value === k) {
                        opt.selected = true;
                    }
                    sel.appendChild(opt);
                });

                // Allow custom path too
                const custom = document.createElement('option');
                custom.value = '__custom__';
                custom.textContent = L.custom_path;
                sel.appendChild(custom);

                sel.addEventListener('change', function() {
                    if (this.value === '__custom__') {
                        const inp = document.createElement('input');
                        inp.type = 'text';
                        inp.name = 'field_values';
                        inp.value = '';
                        inp.placeholder = '~/.ssh/id_xxx';
                        inp.style.cssText = this.style.cssText;
                        row_ref = this.parentNode;
                        row_ref.replaceChild(inp, this);
                        inp.focus();
                    }
                });

                return sel;
            } else {
                const inp = document.createElement('input');
                inp.type = 'text';
                inp.name = 'field_values';
                inp.value = value;
                inp.placeholder = L.value_placeholder;
                inp.style.cssText = 'flex:2;padding:8px;border:1px solid #ccc;border-radius:4px;font-size:14px;';
                return inp;
            }
        }

        function swapValueInput(row, newKey, newValue) {
            const oldVal = row.querySelector('[name="field_values"]');
            if (!oldVal) return;
            const newVal = createValueInput(newKey, newValue);
            row.replaceChild(newVal, oldVal);
        }

        // Add datalist for autocomplete
        const datalist = document.createElement('datalist');
        datalist.id = 'field-keys-list';
        commonFields.forEach(f => {
            const opt = document.createElement('option');
            opt.value = f;
            datalist.appendChild(opt);
        });
        document.body.appendChild(datalist);

        // Load existing fields
        fields.forEach(f => addField(f[0], f[1]));

        // If no fields, add a blank one
        if (fields.length === 0) {
            addField('HostName', '');
            addField('User', '');
        }
    </script>
</body>
</html>