fix(console): comprehensive TUI rendering fixes

- Fix Enter key detection: handle multiple Enter key formats (\n, \r, \r\n)
- Reduce flickering: lower render frequency from 60 FPS to 30 FPS
- Fix menu bar visibility: re-render menu bar after content to prevent overwriting
- Fix content positioning: explicit line positioning for categories and commands
- Fix line shifting: clear lines before writing, control newlines manually
- Limit visible items: prevent overflow with maxVisibleCategories/Commands
- Improve CPU usage: increase sleep interval when no events processed

This fixes:
- Enter key not working for selection
- Strong flickering of the application
- Menu bar not visible or being overwritten
- Top half of selection list not displayed
- Lines being shifted/misaligned
This commit is contained in:
2025-11-10 11:06:07 +01:00
parent 6bc78f5540
commit 8f3c15ddbb
106 changed files with 9082 additions and 4483 deletions

View File

@@ -0,0 +1,198 @@
---
# Backup Before Redeploy
# Creates comprehensive backup of Gitea data, SSL certificates, and configurations
# before redeploying Traefik and Gitea stacks
- name: Backup Before Redeploy
hosts: production
gather_facts: yes
become: no
vars:
gitea_stack_path: "{{ stacks_base_path }}/gitea"
traefik_stack_path: "{{ stacks_base_path }}/traefik"
backup_base_path: "{{ backups_path | default('/home/deploy/backups') }}"
backup_name: "redeploy-backup-{{ ansible_date_time.epoch }}"
tasks:
- name: Display backup plan
ansible.builtin.debug:
msg: |
================================================================================
BACKUP BEFORE REDEPLOY
================================================================================
This playbook will backup:
1. Gitea data (volumes)
2. SSL certificates (acme.json)
3. Gitea configuration (app.ini)
4. Traefik configuration
5. PostgreSQL data (if applicable)
Backup location: {{ backup_base_path }}/{{ backup_name }}
================================================================================
- name: Ensure backup directory exists
ansible.builtin.file:
path: "{{ backup_base_path }}/{{ backup_name }}"
state: directory
mode: '0755'
become: yes
- name: Create backup timestamp file
ansible.builtin.copy:
content: |
Backup created: {{ ansible_date_time.iso8601 }}
Backup name: {{ backup_name }}
Purpose: Before Traefik/Gitea redeploy
dest: "{{ backup_base_path }}/{{ backup_name }}/backup-info.txt"
mode: '0644'
become: yes
# ========================================
# Backup Gitea Data
# ========================================
- name: Check Gitea volumes
ansible.builtin.shell: |
docker volume ls --filter name=gitea --format "{{ '{{' }}.Name{{ '}}' }}"
register: gitea_volumes
changed_when: false
failed_when: false
- name: Backup Gitea volumes
ansible.builtin.shell: |
for volume in {{ gitea_volumes.stdout_lines | join(' ') }}; do
if [ -n "$volume" ]; then
echo "Backing up volume: $volume"
docker run --rm \
-v "$volume:/source:ro" \
-v "{{ backup_base_path }}/{{ backup_name }}:/backup" \
alpine tar czf "/backup/gitea-volume-${volume}.tar.gz" -C /source .
fi
done
when: gitea_volumes.stdout_lines | length > 0
register: gitea_volumes_backup
changed_when: gitea_volumes_backup.rc == 0
# ========================================
# Backup SSL Certificates
# ========================================
- name: Check if acme.json exists
ansible.builtin.stat:
path: "{{ traefik_stack_path }}/acme.json"
register: acme_json_stat
- name: Backup acme.json
ansible.builtin.copy:
src: "{{ traefik_stack_path }}/acme.json"
dest: "{{ backup_base_path }}/{{ backup_name }}/acme.json"
remote_src: yes
mode: '0600'
when: acme_json_stat.stat.exists
register: acme_backup
changed_when: acme_backup.changed | default(false)
# ========================================
# Backup Gitea Configuration
# ========================================
- name: Backup Gitea app.ini
ansible.builtin.shell: |
cd {{ gitea_stack_path }}
docker compose exec -T gitea cat /data/gitea/conf/app.ini > "{{ backup_base_path }}/{{ backup_name }}/gitea-app.ini" 2>/dev/null || echo "Could not read app.ini"
register: gitea_app_ini_backup
changed_when: false
failed_when: false
- name: Backup Gitea docker-compose.yml
ansible.builtin.copy:
src: "{{ gitea_stack_path }}/docker-compose.yml"
dest: "{{ backup_base_path }}/{{ backup_name }}/gitea-docker-compose.yml"
remote_src: yes
mode: '0644'
register: gitea_compose_backup
changed_when: gitea_compose_backup.changed | default(false)
# ========================================
# Backup Traefik Configuration
# ========================================
- name: Backup Traefik configuration files
ansible.builtin.shell: |
cd {{ traefik_stack_path }}
tar czf "{{ backup_base_path }}/{{ backup_name }}/traefik-config.tar.gz" \
traefik.yml \
docker-compose.yml \
dynamic/ 2>/dev/null || echo "Some files may be missing"
register: traefik_config_backup
changed_when: traefik_config_backup.rc == 0
failed_when: false
# ========================================
# Backup PostgreSQL Data (if applicable)
# ========================================
- name: Check if PostgreSQL stack exists
ansible.builtin.stat:
path: "{{ stacks_base_path }}/postgresql/docker-compose.yml"
register: postgres_compose_exists
- name: Backup PostgreSQL database (if running)
ansible.builtin.shell: |
cd {{ stacks_base_path }}/postgresql
if docker compose ps postgres | grep -q "Up"; then
docker compose exec -T postgres pg_dumpall -U postgres | gzip > "{{ backup_base_path }}/{{ backup_name }}/postgresql-all-{{ ansible_date_time.epoch }}.sql.gz"
echo "PostgreSQL backup created"
else
echo "PostgreSQL not running, skipping backup"
fi
when: postgres_compose_exists.stat.exists
register: postgres_backup
changed_when: false
failed_when: false
# ========================================
# Verify Backup
# ========================================
- name: List backup contents
ansible.builtin.shell: |
ls -lh "{{ backup_base_path }}/{{ backup_name }}/"
register: backup_contents
changed_when: false
- name: Calculate backup size
ansible.builtin.shell: |
du -sh "{{ backup_base_path }}/{{ backup_name }}" | awk '{print $1}'
register: backup_size
changed_when: false
- name: Summary
ansible.builtin.debug:
msg: |
================================================================================
BACKUP SUMMARY
================================================================================
Backup location: {{ backup_base_path }}/{{ backup_name }}
Backup size: {{ backup_size.stdout }}
Backed up:
- Gitea volumes: {% if gitea_volumes_backup.changed %}✅{% else %} No volumes found{% endif %}
- SSL certificates (acme.json): {% if acme_backup.changed | default(false) %}✅{% else %} Not found{% endif %}
- Gitea app.ini: {% if gitea_app_ini_backup.rc == 0 %}✅{% else %}⚠️ Could not read{% endif %}
- Gitea docker-compose.yml: {% if gitea_compose_backup.changed | default(false) %}✅{% else %} Not found{% endif %}
- Traefik configuration: {% if traefik_config_backup.rc == 0 %}✅{% else %}⚠️ Some files may be missing{% endif %}
- PostgreSQL data: {% if postgres_backup.rc == 0 and 'created' in postgres_backup.stdout %}✅{% else %} Not running or not found{% endif %}
Backup contents:
{{ backup_contents.stdout }}
================================================================================
NEXT STEPS
================================================================================
Backup completed successfully. You can now proceed with redeploy:
ansible-playbook -i inventory/production.yml playbooks/setup/redeploy-traefik-gitea-clean.yml \
--vault-password-file secrets/.vault_pass \
-e "backup_name={{ backup_name }}"
================================================================================

View File

@@ -0,0 +1,216 @@
---
- name: Cleanup All Containers and Networks on Production Server
hosts: production
become: no
gather_facts: yes
vars:
cleanup_volumes: true # Set to false to preserve volumes
tasks:
- name: Set stacks_base_path if not defined
set_fact:
stacks_base_path: "{{ stacks_base_path | default('/home/deploy/deployment/stacks') }}"
- name: Display cleanup warning
debug:
msg:
- "=== WARNING: This will stop and remove ALL containers ==="
- "Volumes will be removed: {{ cleanup_volumes }}"
- "This will cause downtime for all services"
- "Stacks path: {{ stacks_base_path }}"
- ""
- name: List all running containers before cleanup
command: docker ps --format 'table {{ "{{" }}.Names{{ "}}" }}\t{{ "{{" }}.Status{{ "}}" }}\t{{ "{{" }}.Ports{{ "}}" }}'
register: containers_before
changed_when: false
- name: Display running containers
debug:
msg: "{{ containers_before.stdout_lines }}"
# Stop all Docker Compose stacks
- name: Stop Traefik stack
command: docker compose -f {{ stacks_base_path }}/traefik/docker-compose.yml down
args:
chdir: "{{ stacks_base_path }}/traefik"
ignore_errors: yes
register: traefik_stop
- name: Stop Gitea stack
command: docker compose -f {{ stacks_base_path }}/gitea/docker-compose.yml down
args:
chdir: "{{ stacks_base_path }}/gitea"
ignore_errors: yes
- name: Stop PostgreSQL Production stack
command: docker compose -f {{ stacks_base_path }}/postgresql-production/docker-compose.yml down
args:
chdir: "{{ stacks_base_path }}/postgresql-production"
ignore_errors: yes
- name: Stop PostgreSQL Staging stack
command: docker compose -f {{ stacks_base_path }}/postgresql-staging/docker-compose.yml down
args:
chdir: "{{ stacks_base_path }}/postgresql-staging"
ignore_errors: yes
- name: Stop Redis stack
command: docker compose -f {{ stacks_base_path }}/redis/docker-compose.yml down
args:
chdir: "{{ stacks_base_path }}/redis"
ignore_errors: yes
- name: Stop Docker Registry stack
command: docker compose -f {{ stacks_base_path }}/registry/docker-compose.yml down
args:
chdir: "{{ stacks_base_path }}/registry"
ignore_errors: yes
- name: Stop MinIO stack
command: docker compose -f {{ stacks_base_path }}/minio/docker-compose.yml down
args:
chdir: "{{ stacks_base_path }}/minio"
ignore_errors: yes
- name: Stop Monitoring stack
command: docker compose -f {{ stacks_base_path }}/monitoring/docker-compose.yml down
args:
chdir: "{{ stacks_base_path }}/monitoring"
ignore_errors: yes
- name: Stop Production stack
command: docker compose -f {{ stacks_base_path }}/production/docker-compose.base.yml -f {{ stacks_base_path }}/production/docker-compose.production.yml down
args:
chdir: "{{ stacks_base_path }}/production"
ignore_errors: yes
- name: Stop Staging stack
command: docker compose -f {{ stacks_base_path }}/staging/docker-compose.base.yml -f {{ stacks_base_path }}/staging/docker-compose.staging.yml down
args:
chdir: "{{ stacks_base_path }}/staging"
ignore_errors: yes
- name: Stop WireGuard stack
command: docker compose -f {{ stacks_base_path }}/wireguard/docker-compose.yml down
args:
chdir: "{{ stacks_base_path }}/wireguard"
ignore_errors: yes
# Remove all containers (including stopped ones)
- name: Get all container IDs
command: docker ps -a -q
register: all_containers
changed_when: false
- name: Remove all containers
command: docker rm -f {{ item }}
loop: "{{ all_containers.stdout_lines }}"
when: all_containers.stdout_lines | length > 0
ignore_errors: yes
# Check for port conflicts
- name: Check what's using port 80
command: sudo ss -tlnp 'sport = :80'
register: port_80_check
changed_when: false
ignore_errors: yes
- name: Display port 80 status
debug:
msg: "{{ port_80_check.stdout_lines if port_80_check.rc == 0 else 'Port 80 is free or cannot be checked' }}"
- name: Check what's using port 443
command: sudo ss -tlnp 'sport = :443'
register: port_443_check
changed_when: false
ignore_errors: yes
- name: Display port 443 status
debug:
msg: "{{ port_443_check.stdout_lines if port_443_check.rc == 0 else 'Port 443 is free or cannot be checked' }}"
# Clean up networks
- name: Remove traefik-public network
community.docker.docker_network:
name: traefik-public
state: absent
ignore_errors: yes
- name: Remove app-internal network
community.docker.docker_network:
name: app-internal
state: absent
ignore_errors: yes
- name: Get all custom networks
command: docker network ls --format '{{ "{{" }}.Name{{ "}}" }}'
register: all_networks
changed_when: false
- name: Remove custom networks (except default ones)
community.docker.docker_network:
name: "{{ item }}"
state: absent
loop: "{{ all_networks.stdout_lines }}"
when:
- item not in ['bridge', 'host', 'none']
- item not in ['traefik-public', 'app-internal'] # Already removed above
ignore_errors: yes
# Clean up volumes (if requested)
- name: Get all volumes
command: docker volume ls -q
register: all_volumes
changed_when: false
when: cleanup_volumes | bool
- name: Remove all volumes
command: docker volume rm {{ item }}
loop: "{{ all_volumes.stdout_lines }}"
when:
- cleanup_volumes | bool
- all_volumes.stdout_lines | length > 0
ignore_errors: yes
# Final verification
- name: List remaining containers
command: docker ps -a
register: containers_after
changed_when: false
- name: Display remaining containers
debug:
msg: "{{ containers_after.stdout_lines }}"
- name: List remaining networks
command: docker network ls
register: networks_after
changed_when: false
- name: Display remaining networks
debug:
msg: "{{ networks_after.stdout_lines }}"
- name: Verify ports 80 and 443 are free
command: sudo ss -tlnp 'sport = :{{ item }}'
register: port_check
changed_when: false
failed_when: port_check.rc == 0 and port_check.stdout_lines | length > 0
loop:
- 80
- 443
- name: Display cleanup summary
debug:
msg:
- "=== Cleanup Complete ==="
- "All containers stopped and removed"
- "Networks cleaned up"
- "Volumes removed: {{ cleanup_volumes }}"
- ""
- "Next steps:"
- "1. Run sync-stacks.yml to sync configurations"
- "2. Run setup-infrastructure.yml to deploy fresh infrastructure"

View File

@@ -0,0 +1,255 @@
---
# Rollback Redeploy
# Restores Traefik and Gitea from backup created before redeploy
#
# Usage:
# ansible-playbook -i inventory/production.yml playbooks/maintenance/rollback-redeploy.yml \
# --vault-password-file secrets/.vault_pass \
# -e "backup_name=redeploy-backup-1234567890"
- name: Rollback Redeploy
hosts: production
gather_facts: yes
become: no
vars:
traefik_stack_path: "{{ stacks_base_path }}/traefik"
gitea_stack_path: "{{ stacks_base_path }}/gitea"
backup_base_path: "{{ backups_path | default('/home/deploy/backups') }}"
backup_name: "{{ backup_name | default('') }}"
tasks:
- name: Validate backup name
ansible.builtin.fail:
msg: "backup_name is required. Use: -e 'backup_name=redeploy-backup-1234567890'"
when: backup_name == ""
- name: Check if backup directory exists
ansible.builtin.stat:
path: "{{ backup_base_path }}/{{ backup_name }}"
register: backup_dir_stat
- name: Fail if backup not found
ansible.builtin.fail:
msg: "Backup directory not found: {{ backup_base_path }}/{{ backup_name }}"
when: not backup_dir_stat.stat.exists
- name: Display rollback plan
ansible.builtin.debug:
msg: |
================================================================================
ROLLBACK REDEPLOY
================================================================================
This playbook will restore from backup: {{ backup_base_path }}/{{ backup_name }}
Steps:
1. Stop Traefik and Gitea stacks
2. Restore Gitea volumes
3. Restore SSL certificates (acme.json)
4. Restore Gitea configuration (app.ini)
5. Restore Traefik configuration
6. Restore PostgreSQL data (if applicable)
7. Restart stacks
8. Verify
⚠️ WARNING: This will overwrite current state!
================================================================================
# ========================================
# 1. STOP STACKS
# ========================================
- name: Stop Traefik stack
ansible.builtin.shell: |
cd {{ traefik_stack_path }}
docker compose down
register: traefik_stop
changed_when: traefik_stop.rc == 0
failed_when: false
- name: Stop Gitea stack
ansible.builtin.shell: |
cd {{ gitea_stack_path }}
docker compose down
register: gitea_stop
changed_when: gitea_stop.rc == 0
failed_when: false
# ========================================
# 2. RESTORE GITEA VOLUMES
# ========================================
- name: List Gitea volume backups
ansible.builtin.shell: |
ls -1 "{{ backup_base_path }}/{{ backup_name }}/gitea-volume-"*.tar.gz 2>/dev/null || echo ""
register: gitea_volume_backups
changed_when: false
- name: Restore Gitea volumes
ansible.builtin.shell: |
for backup_file in {{ backup_base_path }}/{{ backup_name }}/gitea-volume-*.tar.gz; do
if [ -f "$backup_file" ]; then
volume_name=$(basename "$backup_file" .tar.gz | sed 's/gitea-volume-//')
echo "Restoring volume: $volume_name"
docker volume create "$volume_name" 2>/dev/null || true
docker run --rm \
-v "$volume_name:/target" \
-v "{{ backup_base_path }}/{{ backup_name }}:/backup:ro" \
alpine sh -c "cd /target && tar xzf /backup/$(basename $backup_file)"
fi
done
when: gitea_volume_backups.stdout != ""
register: gitea_volumes_restore
changed_when: gitea_volumes_restore.rc == 0
# ========================================
# 3. RESTORE SSL CERTIFICATES
# ========================================
- name: Restore acme.json
ansible.builtin.copy:
src: "{{ backup_base_path }}/{{ backup_name }}/acme.json"
dest: "{{ traefik_stack_path }}/acme.json"
remote_src: yes
mode: '0600'
register: acme_restore
changed_when: acme_restore.rc == 0
# ========================================
# 4. RESTORE CONFIGURATIONS
# ========================================
- name: Restore Gitea docker-compose.yml
ansible.builtin.copy:
src: "{{ backup_base_path }}/{{ backup_name }}/gitea-docker-compose.yml"
dest: "{{ gitea_stack_path }}/docker-compose.yml"
remote_src: yes
mode: '0644'
register: gitea_compose_restore
changed_when: gitea_compose_restore.rc == 0
failed_when: false
- name: Restore Traefik configuration
ansible.builtin.shell: |
cd {{ traefik_stack_path }}
tar xzf "{{ backup_base_path }}/{{ backup_name }}/traefik-config.tar.gz" 2>/dev/null || echo "Some files may be missing"
register: traefik_config_restore
changed_when: traefik_config_restore.rc == 0
failed_when: false
# ========================================
# 5. RESTORE POSTGRESQL DATA
# ========================================
- name: Find PostgreSQL backup
ansible.builtin.shell: |
ls -1 "{{ backup_base_path }}/{{ backup_name }}/postgresql-all-"*.sql.gz 2>/dev/null | head -1 || echo ""
register: postgres_backup_file
changed_when: false
- name: Restore PostgreSQL database
ansible.builtin.shell: |
cd {{ stacks_base_path }}/postgresql
if docker compose ps postgres | grep -q "Up"; then
gunzip -c "{{ postgres_backup_file.stdout }}" | docker compose exec -T postgres psql -U postgres
echo "PostgreSQL restored"
else
echo "PostgreSQL not running, skipping restore"
fi
when: postgres_backup_file.stdout != ""
register: postgres_restore
changed_when: false
failed_when: false
# ========================================
# 6. RESTART STACKS
# ========================================
- name: Deploy Traefik stack
community.docker.docker_compose_v2:
project_src: "{{ traefik_stack_path }}"
state: present
pull: always
register: traefik_deploy
- name: Wait for Traefik to be ready
ansible.builtin.shell: |
cd {{ traefik_stack_path }}
docker compose ps traefik | grep -Eiq "Up|running"
register: traefik_ready
changed_when: false
until: traefik_ready.rc == 0
retries: 12
delay: 5
failed_when: traefik_ready.rc != 0
- name: Deploy Gitea stack
community.docker.docker_compose_v2:
project_src: "{{ gitea_stack_path }}"
state: present
pull: always
register: gitea_deploy
- name: Restore Gitea app.ini
ansible.builtin.shell: |
if [ -f "{{ backup_base_path }}/{{ backup_name }}/gitea-app.ini" ]; then
cd {{ gitea_stack_path }}
docker compose exec -T gitea sh -c "cat > /data/gitea/conf/app.ini" < "{{ backup_base_path }}/{{ backup_name }}/gitea-app.ini"
docker compose restart gitea
echo "app.ini restored and Gitea restarted"
else
echo "No app.ini backup found"
fi
register: gitea_app_ini_restore
changed_when: false
failed_when: false
- name: Wait for Gitea to be ready
ansible.builtin.shell: |
cd {{ gitea_stack_path }}
docker compose ps gitea | grep -Eiq "Up|running"
register: gitea_ready
changed_when: false
until: gitea_ready.rc == 0
retries: 12
delay: 5
failed_when: gitea_ready.rc != 0
# ========================================
# 7. VERIFY
# ========================================
- name: Wait for Gitea to be healthy
ansible.builtin.shell: |
cd {{ gitea_stack_path }}
docker compose exec -T gitea curl -f http://localhost:3000/api/healthz 2>&1 | grep -q "status.*pass" && echo "HEALTHY" || echo "NOT_HEALTHY"
register: gitea_health
changed_when: false
until: gitea_health.stdout == "HEALTHY"
retries: 30
delay: 2
failed_when: false
- name: Summary
ansible.builtin.debug:
msg: |
================================================================================
ROLLBACK SUMMARY
================================================================================
Restored from backup: {{ backup_base_path }}/{{ backup_name }}
Restored:
- Gitea volumes: {% if gitea_volumes_restore.changed %}✅{% else %} No volumes to restore{% endif %}
- SSL certificates: {% if acme_restore.changed %}✅{% else %} Not found{% endif %}
- Gitea docker-compose.yml: {% if gitea_compose_restore.changed %}✅{% else %} Not found{% endif %}
- Traefik configuration: {% if traefik_config_restore.rc == 0 %}✅{% else %}⚠️ Some files may be missing{% endif %}
- PostgreSQL data: {% if postgres_restore.rc == 0 and 'restored' in postgres_restore.stdout %}✅{% else %} Not restored{% endif %}
- Gitea app.ini: {% if gitea_app_ini_restore.rc == 0 and 'restored' in gitea_app_ini_restore.stdout %}✅{% else %} Not found{% endif %}
Status:
- Traefik: {% if traefik_ready.rc == 0 %}✅ Running{% else %}❌ Not running{% endif %}
- Gitea: {% if gitea_ready.rc == 0 %}✅ Running{% else %}❌ Not running{% endif %}
- Gitea Health: {% if gitea_health.stdout == 'HEALTHY' %}✅ Healthy{% else %}❌ Not healthy{% endif %}
Next steps:
1. Test Gitea: curl -k https://{{ gitea_domain }}/api/healthz
2. Check logs if issues: cd {{ gitea_stack_path }} && docker compose logs gitea --tail=50
================================================================================