Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 

Repository files navigation

image
_2026_08_01_19_53_20_928.mp4
<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Server Commands</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      background: #0f172a;
      font-family: monospace;
      color: #e2e8f0;
      padding: 20px 24px;
      min-height: 100vh;
    }
    .container {
      width: 100%;
      max-width: 1840px;
      margin: 0 auto;
    }
    h1 {
      font-size: 14px;
      margin-bottom: 14px;
      color: #64748b;
      font-weight: normal;
      display: flex;
      align-items: center;
      gap: 10px;
      flex-wrap: wrap;
    }
    h1 .path span { color: #38bdf8; }
    h1 .date { color: #64748b; }
    h1 .date span { color: #94a3b8; }

    .toolbar {
      display: flex;
      gap: 8px;
      margin-bottom: 12px;
      flex-wrap: wrap;
    }
    .toolbar button {
      background: #3b82f6;
      border: none;
      color: white;
      padding: 7px 12px;
      border-radius: 6px;
      cursor: pointer;
      font-size: 12px;
    }
    .toolbar button:hover { background: #2563eb; }
    .toolbar button.secondary { background: #334155; }
    .toolbar button.secondary:hover { background: #475569; }

    .add-form {
      display: flex;
      gap: 8px;
      margin-bottom: 18px;
      flex-wrap: wrap;
    }
    .add-form input {
      background: #1e293b;
      border: 1px solid #334155;
      color: #e2e8f0;
      padding: 8px 12px;
      border-radius: 6px;
      font-family: monospace;
      font-size: 13px;
      outline: none;
    }
    .add-form input:focus { border-color: #3b82f6; }
    .add-form input.cmd-input { flex: 3; min-width: 280px; }
    .add-form input.hint-input { flex: 1.5; min-width: 180px; }
    .add-form button {
      background: #3b82f6;
      border: none;
      color: white;
      padding: 8px 16px;
      border-radius: 6px;
      cursor: pointer;
      font-size: 13px;
    }
    .add-form button:hover { background: #2563eb; }

    .grid {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 10px;
    }
    .cmd {
      display: flex;
      align-items: center;
      gap: 8px;
      background: #1e293b;
      padding: 8px 10px;
      border-radius: 6px;
      font-size: 13px;
      min-width: 0;
    }
    .cmd code {
      overflow-x: auto;
      white-space: nowrap;
      flex: 1;
      min-width: 0;
      scrollbar-width: none;
      -ms-overflow-style: none;
    }
    .cmd code::-webkit-scrollbar { display: none; }
    .cmd .actions {
      display: flex;
      gap: 4px;
      flex-shrink: 0;
    }

    .icon-btn {
      background: #334155;
      border: none;
      color: #e2e8f0;
      width: 28px;
      height: 28px;
      border-radius: 5px;
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 0;
      position: relative;
      transition: background 0.2s, color 0.2s;
    }
    .icon-btn:hover { background: #3b82f6; }
    .icon-btn.delete:hover { background: #ef4444; }
    .icon-btn.copied {
      background: #1fff8f !important;
      color: #0f172a;
    }
    .icon-btn svg {
      width: 15px;
      height: 15px;
      pointer-events: none;
    }

    .icon-btn .tooltip {
      visibility: hidden;
      opacity: 0;
      position: absolute;
      bottom: calc(100% + 7px);
      left: 50%;
      transform: translateX(-50%);
      background: #1e293b;
      border: 1px solid #475569;
      color: #e2e8f0;
      padding: 6px 10px;
      border-radius: 5px;
      font-size: 12px;
      white-space: nowrap;
      max-width: 320px;
      overflow: hidden;
      text-overflow: ellipsis;
      z-index: 20;
      pointer-events: none;
      transition: opacity 0.15s;
    }
    .icon-btn .tooltip::after {
      content: '';
      position: absolute;
      top: 100%;
      left: 50%;
      margin-left: -5px;
      border: 5px solid transparent;
      border-top-color: #475569;
    }
    .icon-btn:hover .tooltip { visibility: visible; opacity: 1; }
    .icon-btn .tooltip.empty { display: none; }

    .empty {
      grid-column: 1 / -1;
      text-align: center;
      color: #64748b;
      padding: 50px 0;
      font-size: 14px;
    }
    #fileInput { display: none; }

    .modal-overlay {
      display: none;
      position: fixed;
      inset: 0;
      background: rgba(0, 0, 0, 0.65);
      z-index: 100;
      align-items: center;
      justify-content: center;
    }
    .modal-overlay.active { display: flex; }
    .modal {
      background: #1e293b;
      border: 1px solid #334155;
      border-radius: 10px;
      padding: 22px 24px;
      width: 90%;
      max-width: 480px;
      box-shadow: 0 20px 50px rgba(0,0,0,0.5);
    }
    .modal h2 {
      font-size: 16px;
      font-weight: normal;
      color: #94a3b8;
      margin-bottom: 14px;
    }
    .modal .cmd-preview {
      background: #0f172a;
      padding: 10px 12px;
      border-radius: 6px;
      font-size: 13px;
      margin-bottom: 18px;
      word-break: break-all;
      max-height: 80px;
      overflow-y: auto;
      scrollbar-width: none;
      -ms-overflow-style: none;
    }
    .modal .cmd-preview::-webkit-scrollbar { display: none; }
    .modal .info-text {
      background: #0f172a;
      padding: 12px 14px;
      border-radius: 6px;
      font-size: 13px;
      margin-bottom: 18px;
      line-height: 1.5;
      color: #e2e8f0;
    }
    .modal .info-text .ok { color: #1fff8f; }
    .modal-actions {
      display: flex;
      gap: 10px;
      justify-content: flex-end;
    }
    .modal-actions button {
      border: none;
      padding: 8px 16px;
      border-radius: 6px;
      cursor: pointer;
      font-size: 13px;
      font-family: monospace;
    }
    .modal-actions .btn-edit { background: #3b82f6; color: white; }
    .modal-actions .btn-edit:hover { background: #2563eb; }
    .modal-actions .btn-delete { background: #ef4444; color: white; }
    .modal-actions .btn-delete:hover { background: #dc2626; }
    .modal-actions .btn-cancel { background: #334155; color: #e2e8f0; }
    .modal-actions .btn-cancel:hover { background: #475569; }
    .modal-actions .btn-save { background: #22c55e; color: white; }
    .modal-actions .btn-save:hover { background: #16a34a; }
    .modal-actions .btn-ok { background: #1fff8f; color: #0f172a; }
    .modal-actions .btn-ok:hover { background: #00e676; }

    .modal-form {
      display: flex;
      flex-direction: column;
      gap: 10px;
      margin-bottom: 18px;
    }
    .modal-form label {
      font-size: 12px;
      color: #94a3b8;
    }
    .modal-form input {
      background: #0f172a;
      border: 1px solid #334155;
      color: #e2e8f0;
      padding: 9px 12px;
      border-radius: 6px;
      font-family: monospace;
      font-size: 13px;
      outline: none;
      width: 100%;
    }
    .modal-form input:focus { border-color: #3b82f6; }

    @media (max-width: 1100px) {
      .grid { grid-template-columns: 1fr 1fr; }
    }
    @media (max-width: 600px) {
      .grid { grid-template-columns: 1fr; }
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>
      <span class="path">JSON: <span id="jsonPath">commands.json</span></span>
      <span class="date">· последнее сохранение: <span id="jsonDate">—</span></span>
    </h1>

    <div class="toolbar">
      <button onclick="exportCommands()">Сохранить</button>
      <button class="secondary" onclick="document.getElementById('fileInput').click()">Загрузить</button>
      <button class="secondary" onclick="clearAll()">Очистить всё</button>
      <input type="file" id="fileInput" accept=".json" onchange="importCommands(event)">
    </div>

    <div class="add-form">
      <input type="text" id="cmdInput" class="cmd-input" placeholder="Команда..." autocomplete="off">
      <input type="text" id="hintInput" class="hint-input" placeholder="Подсказка (опционально)" autocomplete="off">
      <button onclick="addCommand()">Добавить</button>
    </div>

    <div class="grid" id="commands"></div>
  </div>

  <div class="modal-overlay" id="actionModal">
    <div class="modal">
      <h2>Действие с командой</h2>
      <div class="cmd-preview" id="actionPreview"></div>
      <div class="modal-actions">
        <button class="btn-cancel" onclick="closeModals()">Отмена</button>
        <button class="btn-edit" onclick="openEditModal()">Редактировать</button>
        <button class="btn-delete" onclick="confirmDelete()">Удалить</button>
      </div>
    </div>
  </div>

  <div class="modal-overlay" id="editModal">
    <div class="modal">
      <h2>Редактировать команду</h2>
      <div class="modal-form">
        <div>
          <label>Команда</label>
          <input type="text" id="editText" autocomplete="off">
        </div>
        <div>
          <label>Подсказка</label>
          <input type="text" id="editHint" autocomplete="off">
        </div>
      </div>
      <div class="modal-actions">
        <button class="btn-cancel" onclick="closeModals()">Отмена</button>
        <button class="btn-save" onclick="saveEdit()">Сохранить</button>
      </div>
    </div>
  </div>

  <div class="modal-overlay" id="importModal">
    <div class="modal">
      <h2>Загрузка</h2>
      <div class="info-text" id="importMessage"></div>
      <div class="modal-actions">
        <button class="btn-ok" onclick="closeModals()">OK</button>
      </div>
    </div>
  </div>

  <script>
    const STORAGE_KEY = 'server-commands-v2';
    const PATH_KEY = 'server-commands-path';
    const DATE_KEY = 'server-commands-date';
    let currentIndex = null;

    const defaultCommands = [
      { text: 'apt update', hint: 'Обновить список пакетов' },
      { text: 'apt install -y python3 python3-venv python3-pip', hint: 'Установить Python и pip' },
      { text: 'apt install ufw', hint: 'Установить файрвол' },
      { text: 'python3 -m venv venv', hint: 'Создать виртуальное окружение' },
      { text: 'source venv/bin/activate', hint: 'Активировать venv' },
      { text: 'pip install -r requirements.txt', hint: 'Установить зависимости' },
      { text: 'uvicorn main:app --reload --host 192.168.55.44 --port 888', hint: 'Запуск с hot-reload' },
      { text: 'uvicorn main:app --host 192.168.55.44 --port 545', hint: 'Продакшн запуск' },
      { text: 'kill -9 $(ss -tulnp | grep :444 | awk -F\',\' \'{print $2}\' | awk -F\'=\' \'{print $2}\')', hint: 'Убить процесс на порту 444' }
    ];

    function formatDate(iso) {
      if (!iso) return '—';
      const d = new Date(iso);
      if (isNaN(d.getTime())) return '—';
      const pad = n => String(n).padStart(2, '0');
      return `${pad(d.getDate())}.${pad(d.getMonth()+1)}.${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
    }

    function getJsonPath() {
      return localStorage.getItem(PATH_KEY) || 'commands.json';
    }

    function setJsonPath(name) {
      localStorage.setItem(PATH_KEY, name);
      document.getElementById('jsonPath').textContent = name;
    }

    function setSaveDate(iso) {
      localStorage.setItem(DATE_KEY, iso || '');
      document.getElementById('jsonDate').textContent = formatDate(iso);
    }

    function loadCommands() {
      const saved = localStorage.getItem(STORAGE_KEY);
      if (saved) return JSON.parse(saved);
      localStorage.setItem(STORAGE_KEY, JSON.stringify(defaultCommands));
      return defaultCommands;
    }

    function saveCommands(cmds) {
      localStorage.setItem(STORAGE_KEY, JSON.stringify(cmds));
    }

    function escapeHtml(text) {
      const div = document.createElement('div');
      div.textContent = text;
      return div.innerHTML;
    }

    function render() {
      const cmds = loadCommands();
      const container = document.getElementById('commands');

      if (cmds.length === 0) {
        container.innerHTML = '<div class="empty">Нет команд. Добавь первую!</div>';
        return;
      }

      container.innerHTML = cmds.map((item, i) => {
        const hint = item.hint || '';
        const hasHint = hint.trim().length > 0;
        return `
          <div class="cmd">
            <code>${escapeHtml(item.text)}</code>
            <div class="actions">
              <button class="icon-btn" onclick="copyCmd(this, ${i})">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                  <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
                  <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
                </svg>
                <span class="tooltip ${hasHint ? '' : 'empty'}">${escapeHtml(hint || 'Нет подсказки')}</span>
              </button>
              <button class="icon-btn delete" onclick="openActionModal(${i})">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                  <line x1="18" y1="6" x2="6" y2="18"></line>
                  <line x1="6" y1="6" x2="18" y2="18"></line>
                </svg>
              </button>
            </div>
          </div>
        `;
      }).join('');
    }

    function addCommand() {
      const cmdInput = document.getElementById('cmdInput');
      const hintInput = document.getElementById('hintInput');
      const text = cmdInput.value.trim();
      if (!text) return;

      const cmds = loadCommands();
      cmds.push({ text: text, hint: hintInput.value.trim() });
      saveCommands(cmds);
      cmdInput.value = '';
      hintInput.value = '';
      render();
      cmdInput.focus();
    }

    function openActionModal(index) {
      currentIndex = index;
      const cmds = loadCommands();
      document.getElementById('actionPreview').textContent = cmds[index].text;
      document.getElementById('actionModal').classList.add('active');
    }

    function openEditModal() {
      const cmds = loadCommands();
      const item = cmds[currentIndex];
      document.getElementById('editText').value = item.text;
      document.getElementById('editHint').value = item.hint || '';
      document.getElementById('actionModal').classList.remove('active');
      document.getElementById('editModal').classList.add('active');
      document.getElementById('editText').focus();
    }

    function confirmDelete() {
      const cmds = loadCommands();
      cmds.splice(currentIndex, 1);
      saveCommands(cmds);
      closeModals();
      render();
    }

    function saveEdit() {
      const text = document.getElementById('editText').value.trim();
      if (!text) return;

      const cmds = loadCommands();
      cmds[currentIndex] = {
        text: text,
        hint: document.getElementById('editHint').value.trim()
      };
      saveCommands(cmds);
      closeModals();
      render();
    }

    function closeModals() {
      document.getElementById('actionModal').classList.remove('active');
      document.getElementById('editModal').classList.remove('active');
      document.getElementById('importModal').classList.remove('active');
      currentIndex = null;
    }

    document.querySelectorAll('.modal-overlay').forEach(el => {
      el.addEventListener('click', e => {
        if (e.target === el) closeModals();
      });
    });

    document.addEventListener('keydown', e => {
      if (e.key === 'Escape') closeModals();
    });

    function copyCmd(btn, index) {
      const cmds = loadCommands();
      navigator.clipboard.writeText(cmds[index].text).then(() => {
        btn.classList.add('copied');
        const svg = btn.querySelector('svg');
        svg.innerHTML = `<polyline points="20 6 9 17 4 12"></polyline>`;
        setTimeout(() => {
          btn.classList.remove('copied');
          svg.innerHTML = `
            <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
            <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
          `;
        }, 1200);
      });
    }

    // Сохранить — пишет savedAt в файл, но НЕ трогает дату на экране
    function exportCommands() {
      const cmds = loadCommands();
      const now = new Date().toISOString();
      const payload = {
        savedAt: now,
        commands: cmds
      };
      const filename = getJsonPath();
      const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = filename;
      a.click();
      URL.revokeObjectURL(url);
      // дату на экране НЕ обновляем — она меняется только при Загрузить
    }

    // Загрузить — единственное место, где обновляется дата (из файла)
    function importCommands(event) {
      const file = event.target.files[0];
      if (!file) return;

      const reader = new FileReader();
      reader.onload = e => {
        try {
          let raw = JSON.parse(e.target.result);
          let cmds, savedAt;

          if (raw && Array.isArray(raw.commands)) {
            cmds = raw.commands;
            savedAt = raw.savedAt || null;
          } else if (Array.isArray(raw)) {
            if (raw.length && typeof raw[0] === 'string') {
              cmds = raw.map(t => ({ text: t, hint: '' }));
            } else {
              cmds = raw;
            }
            savedAt = null;
          } else {
            throw new Error('Неверный формат');
          }

          saveCommands(cmds);
          setJsonPath(file.name);
          setSaveDate(savedAt);   // дата из JSON-файла

          document.getElementById('importMessage').innerHTML =
            `<span class="ok">✓</span> Загружено <b>${cmds.length}</b> команд<br>` +
            `Файл: <b>${escapeHtml(file.name)}</b><br>` +
            `Последнее сохранение: <b>${formatDate(savedAt)}</b>`;
          document.getElementById('importModal').classList.add('active');
          render();
        } catch (err) {
          document.getElementById('importMessage').innerHTML =
            `Ошибка: нужен JSON с полем <code>commands</code> или массив команд`;
          document.getElementById('importModal').classList.add('active');
        }
      };
      reader.readAsText(file);
      event.target.value = '';
    }

    function clearAll() {
      if (confirm('Удалить ВСЕ команды?')) {
        saveCommands([]);
        render();
      }
    }

    document.getElementById('cmdInput').addEventListener('keydown', e => {
      if (e.key === 'Enter') addCommand();
    });
    document.getElementById('hintInput').addEventListener('keydown', e => {
      if (e.key === 'Enter') addCommand();
    });
    document.getElementById('editText').addEventListener('keydown', e => {
      if (e.key === 'Enter') saveEdit();
    });
    document.getElementById('editHint').addEventListener('keydown', e => {
      if (e.key === 'Enter') saveEdit();
    });

    document.getElementById('jsonPath').textContent = getJsonPath();
    document.getElementById('jsonDate').textContent = formatDate(localStorage.getItem(DATE_KEY));
    render();
  </script>
</body>
</html>

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors