--- - name: Install Koji Hub, Web, httpd, and crypto packages ansible.builtin.dnf: name: - koji-hub - koji-web - httpd - mod_ssl - mod_wsgi - python3-cryptography - nmap-ncat - postgresql - policycoreutils-python-utils - koji-utils - createrepo_c - tree - python3-koji-hub-plugins state: present - name: Create directories for certs and client configs ansible.builtin.file: path: "{{ item }}" state: directory owner: apache group: apache mode: "0700" loop: - "{{ koji_top_dir }}/certs" - "{{ koji_top_dir }}/clients" - "{{ koji_top_dir }}/packages" - "{{ koji_top_dir }}/repos" - "{{ koji_top_dir }}/work" - "{{ koji_top_dir }}/scratch" - "{{ koji_top_dir }}/repos-dist" - name: Create kojira group ansible.builtin.group: name: kojira state: present - name: Create kojira system user ansible.builtin.user: name: kojira group: kojira home: /var/lib/kojira shell: /sbin/nologin system: true state: present - name: Ensure admin user exists ansible.builtin.user: name: "{{ koji_admin_user }}" state: present create_home: true - name: Add kojiadmin to wheel group for sudo access ansible.builtin.user: name: "{{ koji_admin_user }}" groups: wheel append: true state: present # This is for demonstration purposes only. In a production environment, # it is recommended to configure sudo access with a more restrictive policy. - name: Ensure wheel group has NOPASSWD sudo access ansible.builtin.lineinfile: path: /etc/sudoers regexp: '^# %wheel\s+ALL=\(ALL\)\s+NOPASSWD:\s+ALL$' line: '%wheel ALL=(ALL) NOPASSWD: ALL' validate: 'visudo -cf %s' state: present - name: Ensure .koji directory exists for '{{ koji_admin_user }}' ansible.builtin.file: path: "{{ koji_hub_admin_user_koji_dir }}" state: directory owner: "{{ koji_admin_user }}" group: "{{ koji_admin_user }}" mode: "{{ koji_client_dir_mode }}" - name: Configure Koji client for admin user ansible.builtin.template: src: koji.conf.j2 dest: "{{ koji_hub_admin_user_koji_dir }}/config" owner: "{{ koji_admin_user }}" group: "{{ koji_admin_user }}" mode: "{{ koji_client_file_mode }}" - name: Copy client certificates for '{{ koji_admin_user }}' ansible.builtin.copy: src: "{{ item.src }}" dest: "{{ koji_hub_admin_user_koji_dir }}/{{ item.dest }}" owner: "{{ koji_admin_user }}" group: "{{ koji_admin_user }}" mode: "{{ koji_client_file_mode }}" loop: - src: "{{ koji_pki_dir }}/kojiadmin.pem" dest: client.pem - src: "{{ koji_pki_dir }}/kojiadmin.key" dest: client.key - src: "{{ koji_pki_dir }}/{{ koji_ca_name }}_cert.crt" dest: clientca.crt - src: "{{ koji_pki_dir }}/{{ koji_ca_name }}_cert.crt" dest: serverca.crt - name: Create Koji PKI directory on the hub ansible.builtin.file: path: "{{ koji_remote_pki_dir }}" state: directory owner: root group: root mode: "0755" - name: Copy Koji certificates to the hub ansible.builtin.copy: src: "{{ koji_pki_dir }}/{{ item.src }}" dest: "{{ item.dest }}" owner: "{{ item.owner | default('root') }}" group: "{{ item.group | default('root') }}" mode: "{{ item.mode }}" loop: - { src: "{{ koji_ca_name }}_cert.crt", dest: "{{ koji_remote_ca_trust_path }}", mode: "0644" } - { src: "kojihub.crt", dest: "{{ koji_hub_cert_path }}", mode: "0644", group: "apache" } - { src: "kojihub.key", dest: "{{ koji_hub_key_path }}", mode: "0640", group: "apache" } - { src: "kojihub.pem", dest: "/etc/pki/koji/kojihub.pem", mode: "0644", group: "apache" } - { src: "kojiadmin.pem", dest: "{{ koji_admin_cert_path }}", mode: "0644" } - { src: "kojira.pem", dest: "/etc/pki/koji/kojira.pem", mode: "0640", group: "kojira" } - { src: "kojiweb.pem", dest: "/etc/pki/koji/kojiweb.pem", mode: "0640", group: "apache" } - name: Update CA trust store ansible.builtin.command: update-ca-trust changed_when: false - name: Flush handlers ansible.builtin.meta: flush_handlers - name: Create Koji Hub and Web config directories ansible.builtin.file: path: "{{ item }}" state: directory owner: root group: root mode: "0755" loop: - "{{ koji_hub_config_dir }}" - "{{ koji_hub_koji_web_config_dir }}" - name: Configure httpd for Koji ansible.builtin.lineinfile: path: "{{ item.path }}" regexp: "{{ item.regexp }}" line: "{{ item.line }}" backup: true loop: - { path: "{{ koji_hub_httpd_ssl_config_file }}", regexp: "^SSLCertificateFile", line: "SSLCertificateFile {{ koji_hub_cert_path }}" } - { path: "{{ koji_hub_httpd_ssl_config_file }}", regexp: "^SSLCertificateKeyFile", line: "SSLCertificateKeyFile {{ koji_hub_key_path }}" } - { path: "{{ koji_hub_httpd_ssl_config_file }}", regexp: "^#?SSLCACertificateFile", line: "SSLCACertificateFile {{ koji_remote_ca_trust_path }}" } - { path: "{{ koji_hub_httpd_ssl_config_file }}", regexp: "^#?SSLProtocol", line: "SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1 -TLSv1.3" } notify: Restart httpd - name: Enable SSL for Koji Hub ansible.builtin.blockinfile: path: "{{ koji_hub_httpd_config_file }}" marker: "# {mark} ANSIBLE MANAGED BLOCK" block: | SSLVerifyClient require SSLVerifyDepth 10 SSLOptions +StdEnvVars notify: Restart httpd - name: Enable SSL for Koji Web ansible.builtin.blockinfile: path: "{{ koji_remote_kojiweb_httpd_conf_path }}" marker: "# {mark} ANSIBLE MANAGED BLOCK SSL LOGIN" block: | SSLVerifyClient require SSLVerifyDepth 10 SSLOptions +StdEnvVars notify: Restart httpd - name: Template Koji Hub and Web configs ansible.builtin.template: src: "{{ item.src }}" dest: "{{ item.dest }}" group: apache mode: "0640" loop: - { src: "00-main.conf.j2", dest: "{{ koji_hub_config_dir }}/00-main.conf" } - { src: "db.conf.j2", dest: "{{ koji_hub_config_dir }}/db.conf" } - { src: "admin.conf.j2", dest: "{{ koji_hub_config_dir }}/admin.conf" } - { src: "web.conf.j2", dest: "{{ koji_hub_koji_web_config_dir }}/web.conf" } - { src: "policies.conf.j2", dest: "{{ koji_hub_config_dir }}/policies.conf" } - { src: "99-local-policies.conf.j2", dest: "{{ koji_hub_config_dir }}/99-local-policies.conf" } notify: Restart httpd - name: Deploy Apache configuration for CentOS 10 mirror ansible.builtin.template: src: centos10.conf.j2 dest: /etc/httpd/conf.d/centos10.conf owner: root group: root mode: "0644" notify: Restart httpd - name: Set SELinux booleans for Apache ansible.posix.seboolean: name: "{{ item }}" state: true persistent: true loop: - httpd_can_network_connect_db - httpd_can_network_connect - allow_httpd_anon_write - httpd_use_nfs notify: Restart httpd - name: Configure httpd MaxConnectionsPerChild to prevent memory leaks ansible.builtin.lineinfile: path: /etc/httpd/conf/httpd.conf regexp: '^#?\s*MaxConnectionsPerChild\s+\d+' line: 'MaxConnectionsPerChild 100' state: present backup: true notify: Restart httpd - name: Start and enable httpd ansible.builtin.service: name: httpd state: started enabled: true - name: Open firewall for http/https ansible.posix.firewalld: service: "{{ item }}" permanent: true state: enabled loop: ["http", "https"] notify: Reload firewalld - name: Set SELinux context for Koji topdir ansible.builtin.command: semanage fcontext -a -t httpd_sys_rw_content_t "/mnt/koji(/.*)?" changed_when: false - name: Apply SELinux context ansible.builtin.command: restorecon -r -v /mnt/koji changed_when: false - name: Add database host to /etc/hosts ansible.builtin.lineinfile: path: /etc/hosts line: "{{ hostvars[groups['kojidb'][0]].ansible_default_ipv4.address }} {{ koji_db_host }}" regexp: ".*{{ koji_db_host }}" state: present - name: Add hub host to /etc/hosts ansible.builtin.lineinfile: path: /etc/hosts line: "{{ ansible_default_ipv4.address }} {{ koji_hub_fqdn }}" regexp: ".*{{ koji_hub_fqdn }}" state: present - name: Wait for database to be available ansible.builtin.wait_for: host: "{{ koji_db_host }}" port: 5432 timeout: 120 - name: Check if Koji DB schema is loaded community.postgresql.postgresql_query: login_db: "{{ koji_db_name }}" query: "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'users');" login_host: "{{ koji_db_host }}" login_user: "{{ koji_db_user }}" login_password: "{{ koji_db_password }}" register: koji_hub_schema_check changed_when: false - name: Load Koji DB Schema community.postgresql.postgresql_script: db: "{{ koji_db_name }}" path: /usr/share/koji/schema.sql login_host: "{{ koji_db_host }}" login_user: "{{ koji_db_user }}" login_password: "{{ koji_db_password }}" when: not koji_hub_schema_check.query_result[0].exists - name: Create kojiadmin user in database community.postgresql.postgresql_query: login_db: "{{ koji_db_name }}" query: "INSERT INTO users (name, status, usertype) VALUES ('{{ koji_admin_user }}', 0, 0) ON CONFLICT DO NOTHING" login_host: "{{ koji_db_host }}" login_user: "{{ koji_db_user }}" login_password: "{{ koji_db_password }}" - name: Configure Koji client via ansible-settings.conf ansible.builtin.template: src: koji.conf.j2 dest: "{{ koji_hub_koji_client_config_file }}" group: apache mode: "0644" - name: Grant admin privileges to kojiadmin user community.postgresql.postgresql_query: login_db: "{{ koji_db_name }}" query: > INSERT INTO user_perms (user_id, perm_id, creator_id) VALUES ( (SELECT id FROM users WHERE name = '{{ koji_admin_user }}'), (SELECT id FROM permissions WHERE name = 'admin'), (SELECT id FROM users WHERE name = '{{ koji_admin_user }}') ) ON CONFLICT DO NOTHING login_host: "{{ koji_db_host }}" login_user: "{{ koji_db_user }}" login_password: "{{ koji_db_password }}" - name: Configure Koji Hub Main Options (hub.conf) ansible.builtin.template: src: hub.conf.j2 dest: /etc/koji-hub/hub.conf owner: root group: root mode: "0644" notify: Restart httpd - name: Copy Sigul signing plugin to Koji Hub plugins directory ansible.builtin.copy: src: sigul_sign.py dest: /usr/lib/koji-hub-plugins/sigul_sign.py owner: root group: root mode: "0644" notify: Restart httpd - name: Create spool directories for Koji signing ansible.builtin.file: path: "{{ item }}" state: directory owner: apache group: apache mode: "0770" loop: - /var/spool/koji-sign - /var/spool/koji-sign/queue - /var/spool/koji-sign/failed - name: Copy Koji sign spooler daemon to libexec ansible.builtin.copy: src: koji-sign-spooler dest: /usr/libexec/koji-sign-spooler owner: root group: root mode: "0755" notify: Restart koji-sign-spooler - name: Copy Koji sign spooler systemd service ansible.builtin.copy: src: koji-sign-spooler.service dest: /usr/lib/systemd/system/koji-sign-spooler.service owner: root group: root mode: "0644" notify: Restart koji-sign-spooler - name: Enable and start koji-sign-spooler service ansible.builtin.systemd: name: koji-sign-spooler state: started enabled: true daemon_reload: true - name: Template Koji callback script ansible.builtin.template: src: koji_callbacks.py.j2 dest: "{{ koji_callbacks_script_path }}" mode: "0755" - name: Configure Koji callbacks ansible.builtin.template: src: callbacks.conf.j2 dest: "{{ koji_callbacks_config_path }}" mode: "0640" notify: Restart httpd - name: Template Koji cleanup script ansible.builtin.template: src: koji_cleanup.sh.j2 dest: "{{ koji_cleanup_script_path }}" mode: "0755" - name: Create systemd service for Koji cleanup ansible.builtin.copy: src: koji-cleanup.service dest: "{{ koji_cleanup_service_path }}" mode: "0644" - name: Create systemd timer for Koji cleanup ansible.builtin.copy: src: koji-cleanup.timer dest: "{{ koji_cleanup_timer_path }}" mode: "0644" - name: Enable and start Koji cleanup timer ansible.builtin.systemd: name: koji-cleanup.timer state: started enabled: true daemon_reload: true - name: Enable and start Koji DB sweeper timer ansible.builtin.service: name: koji-sweep-db.timer state: started enabled: true - name: "Generate: MxOS Theme Graphic Assets on Localhost" delegate_to: localhost run_once: true block: - name: Check if rsvg-convert is installed on localhost ansible.builtin.command: which rsvg-convert register: rsvg_check ignore_errors: true changed_when: false - name: Ensure librsvg2-tools is installed on localhost (Fedora) ansible.builtin.dnf: name: librsvg2-tools state: present become: true when: rsvg_check.rc != 0 - name: Convert main logo SVG to PNG ansible.builtin.command: cmd: "rsvg-convert -h 50 --keep-aspect-ratio -o /tmp/mxos-koji.png /home/renich/Projects/MxOS/sitio/static/images/logo.svg" creates: /tmp/mxos-koji.png - name: Convert favicon logo SVG to PNG ansible.builtin.command: cmd: "rsvg-convert -w 32 --keep-aspect-ratio -o /tmp/mxos-favicon.png /home/renich/Projects/MxOS/mxos-reducido.svg" creates: /tmp/mxos-favicon.png - name: "Deploy: Koji Web MxOS Branding Theme" block: - name: Check if viewport is present in Koji Web header template ansible.builtin.command: grep -q "viewport" /usr/share/koji-web/scripts/includes/header.chtml register: koji_viewport_check failed_when: false changed_when: false - name: Inject viewport meta tag in Koji Web header template ansible.builtin.replace: path: /usr/share/koji-web/scripts/includes/header.chtml regexp: '()' replace: '\1\n ' when: koji_viewport_check.rc != 0 notify: Restart httpd - name: Create MxOS theme directories on Hub ansible.builtin.file: path: "{{ item }}" state: directory owner: root group: root mode: "0755" loop: - /usr/share/koji-web/static/themes/mxos - /usr/share/koji-web/static/themes/mxos/images - name: Copy logo PNG to theme images directory ansible.builtin.copy: src: /tmp/mxos-koji.png dest: /usr/share/koji-web/static/themes/mxos/images/koji.png owner: root group: root mode: "0644" notify: Restart httpd - name: Copy favicon PNG to theme images directory as favicon.ico ansible.builtin.copy: src: /tmp/mxos-favicon.png dest: /usr/share/koji-web/static/themes/mxos/images/favicon.ico owner: root group: root mode: "0644" notify: Restart httpd - name: Deploy MxOS koji.css style overrides ansible.builtin.copy: dest: /usr/share/koji-web/static/themes/mxos/koji.css owner: root group: root mode: "0644" content: | @import url("../../koji.css"); /* Reset de fuente base y legibilidad global */ body { font-family: 'Inter', system-ui, -apple-system, sans-serif; font-size: 16px; /* Fuente base */ line-height: 1.6; color: #334155; /* Slate 700 */ background-color: #f1f5f9; /* Fondo gris claro plano */ background-image: none !important; /* Quitar puntos grises de Koji */ margin: 0; padding: 0; } /* Contenedor principal (#wrap) como tarjeta premium */ #wrap { background-color: #ffffff; /* Fondo blanco unificado */ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.05); /* Sombra suave y elegante */ border: 1px solid #e2e8f0; border-radius: 12px; /* Esquinas redondeadas modernas */ margin: 40px auto; max-width: 1280px; /* Ancho máximo estándar para legibilidad */ width: 95%; overflow: hidden; /* Corta los bordes redondeados del header y nav de forma limpia */ padding: 0; box-sizing: border-box; } #innerwrap { padding: 0; } div#content { padding: 30px 40px 40px 40px; /* Padding de respiro elegante a los lados y abajo */ } h2, h3 { color: #0f172a; font-weight: 700; margin-top: 30px; margin-bottom: 20px; font-size: 1.6em; } /* Encabezados de tablas/sección de Koji */ h4 { background-color: #1e293b !important; /* Slate 800 */ color: #f8fafc !important; border-left: 5px solid #10b981; /* Detalle verde esmeralda de 5px */ padding: 12px 20px; font-weight: 600; font-size: 16px; border-radius: 6px; margin: 30px 0 18px 0; box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1); } h4 a { color: #10b981 !important; } h4 a:hover { color: #059669 !important; } #header { background-color: #0f172a; /* Slate 900 */ border-bottom: 3px solid #10b981; /* Verde esmeralda de MxOS */ height: 70px; /* Altura del header */ clear: left; } #kojiLogo { float: left; height: 50px; /* Logotipo redondo */ width: auto; margin-top: 10px; margin-left: 24px; } #headerSearch { float: right; margin-right: 24px; height: 70px; display: flex; align-items: center; gap: 12px; } /* Estilo para los inputs de búsqueda */ #headerSearch select, #headerSearch input[type="text"] { background-color: #1e293b; /* Slate 800 */ border: 1px solid #334155; /* Slate 700 */ color: #f8fafc; /* Slate 50 */ border-radius: 6px; padding: 6px 12px; height: 34px; font-size: 14px; box-sizing: border-box; transition: border-color 0.2s ease; } #headerSearch select:focus, #headerSearch input[type="text"]:focus { border-color: #10b981; outline: none; } #headerSearch input[type="submit"] { background-color: #10b981; color: white; border: none; border-radius: 6px; padding: 0 20px; height: 34px; font-weight: 600; font-size: 14px; cursor: pointer; box-sizing: border-box; transition: background-color 0.2s ease; } #headerSearch input[type="submit"]:hover { background-color: #059669; } /* Estilización del Menú de Pestañas (#mainNav) */ #mainNav { background-color: #1e293b; /* Slate 800 */ border-bottom: 2px solid #10b981; /* Borde verde esmeralda */ font-weight: bold; height: auto; } #mainNav ul { display: flex; flex-wrap: nowrap; /* Desactivar envoltura en pantallas de laptop y escritorio */ padding: 0; margin: 0; list-style-type: none; } #mainNav ul li { background-color: #1e293b; border: none; margin: 0; float: none; } #mainNav ul li a { display: block; color: #cbd5e1; /* Slate 300 para no seleccionado */ text-decoration: none; padding: 12px 16px !important; /* Padding ajustado para asegurar alineación en una sola línea */ font-size: 14px !important; font-family: inherit; height: auto; transition: all 0.2s ease; white-space: nowrap; /* Evitar que el texto de una pestaña con espacios se divida en varias líneas */ } #mainNav ul li:hover { background-color: #334155; /* Slate 700 */ border-color: transparent; } #mainNav ul li:hover a { color: #ffffff; } /* Pestaña activa de la página actual */ body#summary #mainNav li#summaryTab a, body#tasks #mainNav li#tasksTab a, body#tags #mainNav li#tagsTab a, body#builds #mainNav li#buildsTab a, body#packages #mainNav li#packagesTab a, body#users #mainNav li#usersTab a, body#hosts #mainNav li#hostsTab a, body#buildtargets #mainNav li#buildtargetsTab a, body#reports #mainNav li#reportsTab a, body#search #mainNav li#searchTab a, body#api #mainNav li#apiTab a { background-color: #10b981; /* Emerald 500 */ color: white !important; } /* Cambiar colores de enlaces y botones globales */ a { color: #10b981; text-decoration: none; font-weight: 500; } a:hover { color: #059669; /* Verde oscuro */ text-decoration: underline; } /* --- ESTILOS DE TABLAS DENTRO DEL CONTENEDOR BLANCO --- */ table { width: 100% !important; margin-bottom: 35px; border-collapse: collapse; border-spacing: 0; background-color: #ffffff; } /* Tablas de Información (Estructura Vertical: th a la izquierda, td a la derecha) */ table th { font-weight: 600; color: #0f172a; /* Slate 900 */ background-color: #f8fafc; /* Slate 50 */ border-bottom: 1px solid #e2e8f0; border-right: 1px solid #e2e8f0; text-align: right; padding: 14px 20px !important; /* Padding ampliado */ width: 220px; font-size: 15px !important; /* Letras más grandes */ } table td { padding: 14px 20px !important; border-bottom: 1px solid #e2e8f0; color: #334155; font-size: 15px !important; vertical-align: middle; } /* Eliminar el borde inferior de la última fila de la tabla */ table tr:last-child th, table tr:last-child td { border-bottom: none; } /* Tablas de Listas de Datos (Estructura Horizontal: th arriba, td abajo) */ table.data-list, table.list { width: 100% !important; } table.data-list th, table.list th, tr.list-header th { background-color: #1e293b !important; /* Slate 800 */ color: #f8fafc !important; font-weight: 600; text-align: left; border-bottom: 2px solid #10b981 !important; /* Borde verde esmeralda inferior */ border-right: none; padding: 14px 20px !important; font-size: 15px !important; } table.data-list td, table.list td { border-bottom: 1px solid #e2e8f0; border-right: none; text-align: left; padding: 14px 20px !important; } /* Filas alternas y hover en listas */ table.data-list tr:nth-child(even), table.list tr:nth-child(even) { background-color: #f8fafc; } table.data-list tr:hover, table.list tr:hover { background-color: #f1f5f9; /* Hover slate claro */ } /* Estilos para tablas anidadas (como en la vista de buildinfo de RPMs y Logs) */ table.nested { width: 100% !important; margin: 0 !important; border-collapse: collapse !important; border: none !important; background-color: transparent !important; } table.nested tr { background-color: transparent !important; } table.nested tr:hover { background-color: transparent !important; /* Desactivar hover en filas anidadas */ } table.nested th { width: auto !important; text-align: left !important; background-color: #f1f5f9 !important; /* Slate 100 */ color: #475569 !important; /* Slate 600 */ font-size: 13px !important; padding: 6px 12px !important; border: 1px solid #e2e8f0 !important; border-radius: 6px !important; display: inline-block !important; margin-top: 8px !important; margin-bottom: 4px !important; font-weight: 600 !important; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05) !important; } table.nested td { padding: 6px 12px !important; border: none !important; font-size: 14px !important; background-color: transparent !important; } /* Botones */ .button, input[type="submit"], input[type="button"] { background-color: #10b981; color: white; border: none; border-radius: 6px; padding: 10px 20px; font-weight: 600; font-size: 14px; cursor: pointer; transition: background-color 0.2s ease; } .button:hover, input[type="submit"]:hover, input[type="button"]:hover { background-color: #059669; } /* Login Info styling */ #loginInfo { float: right; font-weight: 500; margin: 15px 5px; color: #64748b; font-size: 13px; } #loginInfo a { color: #10b981; } /* --- Pantallas medianas (tabletas y laptops pequeñas hasta el max-width del wrap) --- */ @media (min-width: 769px) and (max-width: 1280px) { #mainNav ul li a { padding: 10px 10px !important; font-size: 13px !important; } } /* --- ESTILOS RESPONSIVOS PARA MÓVILES (Media Queries) --- */ @media (max-width: 768px) { #wrap { width: 100%; margin: 0; border-radius: 0; box-shadow: none; border: none; padding: 0; } div#content { padding: 20px 15px; /* Padding reducido para móviles */ } #header { height: auto; min-height: 120px; padding: 15px 10px; text-align: center; } #kojiLogo { float: none; display: block; margin: 0 auto 15px auto; height: 44px; } #headerSearch { float: none; margin: 10px auto 0 auto; justify-content: center; height: auto; flex-wrap: wrap; gap: 8px; } #mainNav ul { flex-wrap: wrap; /* Permitir envoltura en móviles */ } #mainNav ul li { flex: 1 1 45%; /* Menú más cómodo en móviles (dos por fila) */ } #mainNav ul li a { padding: 12px 10px !important; font-size: 13px !important; text-align: center; } #loginInfo { float: none; display: block; text-align: center; margin: 20px 0; } /* Tablas Responsivas en Móvil (convierten a scroll horizontal) */ table, table.data-list, table.list { display: block; width: 100% !important; overflow-x: auto; -webkit-overflow-scrolling: touch; } } notify: Restart httpd