- Add Apache configuration block in roles/koji_hub/tasks/main.yaml to require SSL client certificate authentication for /koji/login inside kojiweb.conf. - Fixes the koji.AuthError: could not verify client: None error during Koji Web login. Co-developed-by: Gemini AI <renich+gemini@woralelandia.com> Signed-off-by: Rénich Bon Ćirić <renich@woralelandia.com>
909 lines
28 KiB
YAML
909 lines
28 KiB
YAML
---
|
|
- 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: root
|
|
group: 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 }}" }
|
|
notify: Restart httpd
|
|
|
|
- name: Enable SSL for Koji Hub
|
|
ansible.builtin.blockinfile:
|
|
path: "{{ koji_hub_httpd_config_file }}"
|
|
marker: "# {mark} ANSIBLE MANAGED BLOCK"
|
|
block: |
|
|
<Location /kojihub/ssllogin>
|
|
SSLVerifyClient require
|
|
SSLVerifyDepth 10
|
|
SSLOptions +StdEnvVars
|
|
</Location>
|
|
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: |
|
|
<Location /koji/login>
|
|
SSLVerifyClient require
|
|
SSLVerifyDepth 10
|
|
SSLOptions +StdEnvVars
|
|
</Location>
|
|
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: 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: '(<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>)'
|
|
replace: '\1\n <meta name="viewport" content="width=device-width, initial-scale=1"/>'
|
|
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
|