Files
virtuality/web/templates/_downloads_catalog.html
T
Claude c2dc32947f Скачивание образов было доработано по итогам ревью: зависшие операции, гонка запусков, отмена при проверке и распаковке
- Проверка «уже скачивается» идёт по всем операциям каталога (glob), а не по последним 50;
  скачивания без живого потока закрываются при старте (reap_stale_downloads), их .part удаляются,
  а отмена такой операции завершает её сразу, не дожидаясь потока.
- Два запуска одного файла больше не пишут в один .part: проверки и создание операции под замком,
  файл создаётся монопольно (O_EXCL), вторая копия завершается ошибкой, не трогая чужой файл;
  осиротевший .part без владельца убирается перед новым запуском.
- Отмена во время проверки контрольной суммы и распаковки .img.xz останавливает работу:
  операция завершается как отменённая, недокачанный файл, архив и недораспакованный образ удаляются.
- Битый .img.xz после неудачной распаковки удаляется с понятным сообщением, повторное скачивание разрешено;
  проверка «файл уже есть» учитывает распакованный .img и qcow2 после конвертации.
- Ссылки с кириллицей (домен IDNA, путь и параметры в процентах) открываются, некорректный порт
  и непонятные символы отклоняются сразу; ошибки сети описаны словами, исходный текст — в журнале.
- Прогресс раз в секунду перечитывает JSON операции, чтобы не затирать cancel_requested и замечать
  отмену из другого процесса; операции читаются один раз на страницу; отмена в карточке — не ошибка.
- Формулировки карточек и сообщения о недоступной архитектуре упрощены; добавлены тесты на каждое поведение.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019LEUXMcdZo7b87mjkUsPgg
2026-09-25 15:31:12 +00:00

37 строки
2.6 KiB
HTML

{#- Карточка «Каталог готовых систем» для страницы /disk-images; страница может заранее положить в dl результат downloads_panel('disk'). -#}
{% from "_ui.html" import icon, badge %}
{% set dl = dl if dl is defined else downloads_panel('disk') %}
<article class="card">
<div class="card-header">
<div><h2>Каталог готовых систем</h2><p>Официальные образы, подходящие этому серверу. Скачаем на сервер и проверим контрольную сумму — останется только создать машину.</p></div>
</div>
{% if dl.catalog %}
<div class="list">
{% for item in dl.catalog %}
<div class="list-item catalog-item">
<span class="list-icon {{ 'success' if item.status == 'ready' else 'info' if item.status == 'downloading' else 'brand' }}">{{ icon('circle-check' if item.status == 'ready' else 'download' if item.status == 'downloading' else 'box') }}</span>
<div class="grow">
<div class="row" style="gap:8px">
<span class="title">{{ item.title }}</span>
{% if item.recommended %}{{ badge('Рекомендуем', 'brand', dot=False) }}{% endif %}
{% if item.status == 'ready' %}{{ badge('Загружен', 'success') }}{% elif item.status == 'downloading' %}{{ badge('Скачивается', 'info') }}{% endif %}
</div>
<div class="meta">{{ item.hint }}{% if item.login %} · вход: <code>{{ item.login }}</code>{% endif %}</div>
</div>
{% if item.status == 'ready' %}
<a class="btn btn-sm btn-primary" href="/vm/create?image={{ item.ready_file|urlencode }}">{{ icon('plus', 'icon-sm') }}Создать машину</a>
{% elif item.status == 'downloading' %}
<a class="btn btn-sm btn-ghost" href="/operations/{{ item.operation_id }}">Прогресс{{ icon('chevron-right', 'icon-sm') }}</a>
{% else %}
<form method="post" action="/downloads/catalog/{{ item.id }}" class="inline-form" data-busy="Запускаем…">
<button type="submit" class="btn btn-sm">{{ icon('download', 'icon-sm') }}Скачать</button>
</form>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<div class="card-body"><p class="muted">Подходящих этому серверу готовых образов в каталоге нет. Можно скачать образ по ссылке или загрузить с компьютера.</p></div>
{% endif %}
</article>