From ff4938533cb00740e5a84a97e031850e62118bf1 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Tue, 29 Jul 2025 20:54:20 +0700 Subject: [PATCH 01/12] Fix: Add tar.gz fallback for unsupported systems - The script has been updated to include a fallback mechanism for systems that do not have `dpkg` or `rpm` package managers. This ensures that the agent can be installed on a wider range of Linux distributions, such as Arch Linux, by automatically using the `tar.gz` package when native package managers are not detected. --- scripts/server-agent.sh | 367 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 330 insertions(+), 37 deletions(-) diff --git a/scripts/server-agent.sh b/scripts/server-agent.sh index e73db52..5f5d6f5 100644 --- a/scripts/server-agent.sh +++ b/scripts/server-agent.sh @@ -1,4 +1,3 @@ - #!/bin/bash # CheckCle Server Monitoring Agent - One-Click Installation Script @@ -44,7 +43,7 @@ check_root() { fi } -# Detect system architecture and package format +# Detect system architecture and package format with fallback detect_system() { log_info "Detecting system architecture and package format..." @@ -64,28 +63,89 @@ detect_system() { ;; esac - # Detect package format preference - if command -v dpkg >/dev/null 2>&1; then - PACKAGE_FORMAT="deb" - PACKAGE_MANAGER="dpkg" - elif command -v rpm >/dev/null 2>&1; then - PACKAGE_FORMAT="rpm" - if command -v yum >/dev/null 2>&1; then - PACKAGE_MANAGER="yum" - elif command -v dnf >/dev/null 2>&1; then - PACKAGE_MANAGER="dnf" - else - PACKAGE_MANAGER="rpm" - fi - else - log_error "No supported package manager found (dpkg or rpm required)" - log_info "This script supports Debian/Ubuntu (.deb) and RHEL/CentOS/Fedora (.rpm) systems" - exit 1 + # Detect OS and preferred package format + PREFERRED_FORMAT="" + PREFERRED_MANAGER="" + + if [[ -f /etc/os-release ]]; then + OS_ID=$(grep "^ID=" /etc/os-release | cut -d'=' -f2 | tr -d '"') + OS_LIKE=$(grep "^ID_LIKE=" /etc/os-release 2>/dev/null | cut -d'=' -f2 | tr -d '"' || echo "") + + log_info "Detected OS: $OS_ID" + [[ -n "$OS_LIKE" ]] && log_info "OS family: $OS_LIKE" + + # Determine preferred package format based on OS + case "$OS_ID" in + debian|ubuntu|linuxmint|elementary|pop) + PREFERRED_FORMAT="deb" + PREFERRED_MANAGER="dpkg" + ;; + rhel|centos|fedora|rocky|almalinux|oracle|amazonlinux) + PREFERRED_FORMAT="rpm" + if command -v dnf >/dev/null 2>&1; then + PREFERRED_MANAGER="dnf" + elif command -v yum >/dev/null 2>&1; then + PREFERRED_MANAGER="yum" + else + PREFERRED_MANAGER="rpm" + fi + ;; + arch|manjaro|artix|endeavouros) + # Arch-based systems - use tar.gz fallback + log_info "Arch-based system detected, using tar.gz format" + PREFERRED_FORMAT="tar.gz" + PREFERRED_MANAGER="tar" + ;; + *) + # Check OS_LIKE for family detection + if [[ "$OS_LIKE" == *"debian"* ]] || [[ "$OS_LIKE" == *"ubuntu"* ]]; then + PREFERRED_FORMAT="deb" + PREFERRED_MANAGER="dpkg" + elif [[ "$OS_LIKE" == *"rhel"* ]] || [[ "$OS_LIKE" == *"fedora"* ]]; then + PREFERRED_FORMAT="rpm" + if command -v dnf >/dev/null 2>&1; then + PREFERRED_MANAGER="dnf" + elif command -v yum >/dev/null 2>&1; then + PREFERRED_MANAGER="yum" + else + PREFERRED_MANAGER="rpm" + fi + elif [[ "$OS_LIKE" == *"arch"* ]]; then + PREFERRED_FORMAT="tar.gz" + PREFERRED_MANAGER="tar" + fi + ;; + esac fi - # Construct package filename and URL - PACKAGE_FILENAME="monitoring-agent_1.0.0_${PACKAGE_ARCH}.${PACKAGE_FORMAT}" - PACKAGE_URL="${GITHUB_BASE_URL}/${PACKAGE_FILENAME}" + # If no preferred format detected, check available package managers + if [[ -z "$PREFERRED_FORMAT" ]]; then + if command -v dpkg >/dev/null 2>&1; then + PREFERRED_FORMAT="deb" + PREFERRED_MANAGER="dpkg" + elif command -v rpm >/dev/null 2>&1; then + PREFERRED_FORMAT="rpm" + if command -v dnf >/dev/null 2>&1; then + PREFERRED_MANAGER="dnf" + elif command -v yum >/dev/null 2>&1; then + PREFERRED_MANAGER="yum" + else + PREFERRED_MANAGER="rpm" + fi + else + # Fallback to tar.gz for unsupported systems + log_info "No native package manager found, using tar.gz fallback" + PREFERRED_FORMAT="tar.gz" + PREFERRED_MANAGER="tar" + fi + fi + + # Set initial package format and manager + PACKAGE_FORMAT="$PREFERRED_FORMAT" + PACKAGE_MANAGER="$PREFERRED_MANAGER" + + # Construct initial package filename and URL + construct_package_url log_success "System detection complete:" log_info " Architecture: $ARCH -> $PACKAGE_ARCH" @@ -94,6 +154,16 @@ detect_system() { log_info " Package URL: $PACKAGE_URL" } +# Construct package URL based on current format +construct_package_url() { + if [[ "$PACKAGE_FORMAT" == "tar.gz" ]]; then + PACKAGE_FILENAME="monitoring-agent_1.0.0_${PACKAGE_ARCH}.tar.gz" + else + PACKAGE_FILENAME="monitoring-agent_1.0.0_${PACKAGE_ARCH}.${PACKAGE_FORMAT}" + fi + PACKAGE_URL="${GITHUB_BASE_URL}/${PACKAGE_FILENAME}" +} + # Validate required environment variables - check both current and sudo environments validate_environment() { # Check if SERVER_TOKEN is available in current environment or passed as argument @@ -116,7 +186,7 @@ validate_environment() { [[ -n "$AGENT_ID" ]] && log_info "AGENT_ID: $AGENT_ID" } -# Download package based on detected system +# Download package with fallback to tar.gz download_package() { local temp_dir="/tmp/monitoring-agent-install" mkdir -p "$temp_dir" @@ -124,17 +194,211 @@ download_package() { log_info "Downloading monitoring agent package..." log_info "URL: $PACKAGE_URL" - if curl -L -f -o "$temp_dir/$PACKAGE_FILENAME" "$PACKAGE_URL"; then - DOWNLOADED_PACKAGE="$temp_dir/$PACKAGE_FILENAME" - log_success "Package downloaded successfully: $PACKAGE_FILENAME" + # Try to download the preferred package format first + if curl -L -f -s -o "$temp_dir/$PACKAGE_FILENAME" "$PACKAGE_URL" 2>/dev/null; then + # Verify the file was actually downloaded and has content + if [[ -s "$temp_dir/$PACKAGE_FILENAME" ]]; then + DOWNLOADED_PACKAGE="$temp_dir/$PACKAGE_FILENAME" + log_success "Package downloaded successfully: $PACKAGE_FILENAME" + return 0 + else + log_warning "Downloaded file is empty, trying fallback..." + rm -f "$temp_dir/$PACKAGE_FILENAME" + fi else - log_error "Failed to download package from: $PACKAGE_URL" - log_info "Please check:" - log_info " 1. Internet connectivity" - log_info " 2. Package availability for your architecture ($PACKAGE_ARCH)" - log_info " 3. GitHub repository access" + log_warning "Failed to download preferred package format: $PACKAGE_FORMAT" + fi + + # If preferred format failed and it's not tar.gz, try tar.gz fallback + if [[ "$PACKAGE_FORMAT" != "tar.gz" ]]; then + log_warning "Native package ($PACKAGE_FORMAT) not available, trying tar.gz fallback..." + + # Switch to tar.gz format + PACKAGE_FORMAT="tar.gz" + PACKAGE_MANAGER="tar" + construct_package_url + + log_info "Fallback URL: $PACKAGE_URL" + + if curl -L -f -s -o "$temp_dir/$PACKAGE_FILENAME" "$PACKAGE_URL" 2>/dev/null; then + # Verify the file was actually downloaded and has content + if [[ -s "$temp_dir/$PACKAGE_FILENAME" ]]; then + DOWNLOADED_PACKAGE="$temp_dir/$PACKAGE_FILENAME" + log_success "Fallback package downloaded successfully: $PACKAGE_FILENAME" + return 0 + else + log_error "Downloaded fallback file is empty" + rm -f "$temp_dir/$PACKAGE_FILENAME" + fi + else + log_error "Failed to download tar.gz fallback" + fi + fi + + # Both attempts failed + log_error "Failed to download package from: $PACKAGE_URL" + log_info "Please check:" + log_info " 1. Internet connectivity" + log_info " 2. Package availability for your architecture ($PACKAGE_ARCH)" + log_info " 3. GitHub repository access" + exit 1 +} + +# Install tar.gz package +install_tar_package() { + log_info "Installing monitoring agent from tar.gz package..." + + local temp_extract_dir="/tmp/monitoring-agent-extract" + mkdir -p "$temp_extract_dir" + + # Extract tar.gz package + if tar -xzf "$DOWNLOADED_PACKAGE" -C "$temp_extract_dir"; then + log_success "Package extracted successfully" + else + log_error "Failed to extract tar.gz package" exit 1 fi + + # Install files to their proper locations + log_info "Installing files to system directories..." + + # Create necessary directories + mkdir -p /usr/bin + mkdir -p /etc/monitoring-agent + mkdir -p /var/lib/monitoring-agent + mkdir -p /var/log/monitoring-agent + + # Detect systemd service directory based on OS + if [[ -d /lib/systemd/system ]]; then + SYSTEMD_DIR="/lib/systemd/system" + elif [[ -d /usr/lib/systemd/system ]]; then + SYSTEMD_DIR="/usr/lib/systemd/system" + else + SYSTEMD_DIR="/etc/systemd/system" + fi + mkdir -p "$SYSTEMD_DIR" + + # Copy files from extracted package + if [[ -f "$temp_extract_dir/usr/bin/monitoring-agent" ]]; then + cp "$temp_extract_dir/usr/bin/monitoring-agent" /usr/bin/ + chmod +x /usr/bin/monitoring-agent + log_success "Binary installed to /usr/bin/monitoring-agent" + else + log_error "Binary not found in package" + exit 1 + fi + + if [[ -f "$temp_extract_dir/etc/monitoring-agent/monitoring-agent.env" ]]; then + cp "$temp_extract_dir/etc/monitoring-agent/monitoring-agent.env" /etc/monitoring-agent/ + log_success "Configuration template installed" + fi + + # Create monitoring-agent user and group if they don't exist + if ! getent group monitoring-agent >/dev/null 2>&1; then + groupadd --system monitoring-agent + log_success "Created monitoring-agent group" + else + log_info "monitoring-agent group already exists" + fi + + if ! getent passwd monitoring-agent >/dev/null 2>&1; then + useradd --system --gid monitoring-agent --home-dir /var/lib/monitoring-agent --shell /bin/false --comment "Monitoring Agent" monitoring-agent + log_success "Created monitoring-agent user" + else + log_info "monitoring-agent user already exists" + fi + + # Set proper ownership and permissions + chown -R monitoring-agent:monitoring-agent /var/lib/monitoring-agent + chown -R monitoring-agent:monitoring-agent /var/log/monitoring-agent + chown -R monitoring-agent:monitoring-agent /etc/monitoring-agent + chmod 755 /var/lib/monitoring-agent + chmod 755 /var/log/monitoring-agent + chmod 755 /etc/monitoring-agent + + # Check if Docker is installed and configure service file accordingly + DOCKER_AVAILABLE=false + if command -v docker >/dev/null 2>&1 && getent group docker >/dev/null 2>&1; then + DOCKER_AVAILABLE=true + # Add monitoring-agent user to docker group if Docker is available + usermod -a -G docker monitoring-agent + log_info "Docker detected, added monitoring-agent user to docker group" + else + log_info "Docker not detected, skipping Docker group configuration" + fi + + # Create the systemd service file with proper Docker configuration + create_systemd_service_file "$SYSTEMD_DIR" "$DOCKER_AVAILABLE" + + # Clean up extraction directory + rm -rf "$temp_extract_dir" + + log_success "Tar.gz package installation completed" +} + +# Create systemd service file with Docker configuration +create_systemd_service_file() { + local systemd_dir="$1" + local docker_available="$2" + + log_info "Creating systemd service file at $systemd_dir/monitoring-agent.service" + + cat > "$systemd_dir/monitoring-agent.service" << 'EOF' +[Unit] +Description=CheckCle Server Monitoring Agent +Documentation=https://github.com/operacle/checkcle-server-agent +After=network.target +Wants=network.target + +[Service] +Type=simple +User=monitoring-agent +Group=monitoring-agent +EOF + + # Add Docker group only if Docker is available + if [[ "$docker_available" == "true" ]]; then + echo "# Add docker group for Docker monitoring access" >> "$systemd_dir/monitoring-agent.service" + echo "SupplementaryGroups=docker" >> "$systemd_dir/monitoring-agent.service" + else + echo "# Docker not available - skipping Docker group configuration" >> "$systemd_dir/monitoring-agent.service" + fi + + # Continue with the rest of the service configuration + cat >> "$systemd_dir/monitoring-agent.service" << 'EOF' +ExecStart=/usr/bin/monitoring-agent +Restart=always +RestartSec=10 +StandardOutput=journal +StandardError=journal + +# Security settings +NoNewPrivileges=yes +ProtectSystem=strict +ProtectHome=yes +PrivateTmp=yes +ProtectKernelTunables=yes +ProtectControlGroups=yes +RestrictSUIDSGID=yes +RemoveIPC=yes +RestrictRealtime=yes + +# Allow access to system information and configuration +ReadWritePaths=/var/log/monitoring-agent +ReadOnlyPaths=/proc /sys /etc/monitoring-agent + +# Network access +PrivateNetwork=no + +# Environment +EnvironmentFile=/etc/monitoring-agent/monitoring-agent.env +WorkingDirectory=/var/lib/monitoring-agent + +[Install] +WantedBy=multi-user.target +EOF + + log_success "Created systemd service file with Docker support: $docker_available" } # Install package based on detected package manager @@ -185,6 +449,10 @@ install_package() { fi ;; + tar) + install_tar_package + ;; + *) log_error "Unsupported package manager: $PACKAGE_MANAGER" exit 1 @@ -232,7 +500,7 @@ configure_agent() { log_info "Final configuration:" log_info " Server Name: $SERVER_NAME" log_info " Agent ID: $AGENT_ID" - log_info " PB API URL: $POCKETBASE_URL" + log_info " PocketBase URL: $POCKETBASE_URL" log_info " IP Address: $IP_ADDRESS" log_info " OS Type: $OS_TYPE" log_info " Health Check Port: $HEALTH_CHECK_PORT" @@ -341,7 +609,7 @@ test_installation() { show_post_install_info() { echo echo "=============================================" - echo " CheckCle Server Agent Installation Complete!" + echo " Installation Complete!" echo "=============================================" echo log_success "CheckCle Monitoring Agent installed and configured successfully" @@ -350,6 +618,11 @@ show_post_install_info() { echo " Architecture: $ARCH ($PACKAGE_ARCH)" echo " Package: $PACKAGE_FILENAME" echo " Package Manager: $PACKAGE_MANAGER" + if [[ "$PACKAGE_FORMAT" == "tar.gz" ]]; then + echo " Installation Type: Manual (tar.gz)" + else + echo " Installation Type: Native package ($PACKAGE_FORMAT)" + fi echo echo "Configuration: $CONFIG_FILE" echo "Service status: systemctl status monitoring-agent" @@ -416,7 +689,12 @@ case "${1:-}" in echo "System Requirements:" echo " - Linux with systemd" echo " - Supported architectures: x86_64 (amd64), aarch64/arm64" - echo " - Supported distributions: Debian/Ubuntu (.deb), RHEL/CentOS/Fedora (.rpm)" + echo " - Supported distributions: Debian/Ubuntu (.deb), RHEL/CentOS/Fedora (.rpm), or any Linux (tar.gz fallback)" + echo + echo "Package Installation Logic:" + echo " 1. Detects OS and tries native package format first (.deb for Debian-based, .rpm for RHEL-based)" + echo " 2. If native package is not available, automatically falls back to tar.gz" + echo " 3. tar.gz works on any Linux distribution with systemd" echo echo "Required Environment Variables:" echo " SERVER_TOKEN Server authentication token" @@ -444,13 +722,28 @@ case "${1:-}" in systemctl disable monitoring-agent 2>/dev/null || true # Remove based on detected package manager - if command -v dpkg >/dev/null 2>&1; then + if command -v dpkg >/dev/null 2>&1 && dpkg -l monitoring-agent >/dev/null 2>&1; then dpkg -r monitoring-agent 2>/dev/null || true - elif command -v rpm >/dev/null 2>&1; then + log_success "DEB package removed" + elif command -v rpm >/dev/null 2>&1 && rpm -q monitoring-agent >/dev/null 2>&1; then rpm -e monitoring-agent 2>/dev/null || true + log_success "RPM package removed" + else + # Manual removal for tar.gz installations + log_info "Performing manual cleanup for tar.gz installation..." + rm -f /usr/bin/monitoring-agent 2>/dev/null || true + rm -rf /etc/monitoring-agent 2>/dev/null || true + rm -f /lib/systemd/system/monitoring-agent.service 2>/dev/null || true + rm -f /usr/lib/systemd/system/monitoring-agent.service 2>/dev/null || true + rm -f /etc/systemd/system/monitoring-agent.service 2>/dev/null || true + userdel monitoring-agent 2>/dev/null || true + groupdel monitoring-agent 2>/dev/null || true + rm -rf /var/lib/monitoring-agent 2>/dev/null || true + rm -rf /var/log/monitoring-agent 2>/dev/null || true + log_success "Manual cleanup completed" fi - rm -rf /etc/monitoring-agent 2>/dev/null || true + systemctl daemon-reload 2>/dev/null || true log_success "Monitoring agent uninstalled" exit 0 ;; From 1d8b5172c535bb65111823be0b48497bce932eb1 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Tue, 29 Jul 2025 20:54:54 +0700 Subject: [PATCH 02/12] Added the OS logo icon for Arch Linux --- application/public/upload/os/arch.png | Bin 0 -> 59587 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 application/public/upload/os/arch.png diff --git a/application/public/upload/os/arch.png b/application/public/upload/os/arch.png new file mode 100644 index 0000000000000000000000000000000000000000..8a69099456464af44d5fd18b76c84713273b36a7 GIT binary patch literal 59587 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 pDsShN~|AF@D zPNcgxBIy?mHYK*~ly_tr>lXJjb;GrK&ZeOC6KVW!iY>g z+HMr+dndwiQUgx;utR*d@&25^GNJ{>Ppqu;KxT^<8jMMK99BApz7=cNn$QW!^PeLg z!N5+5YY{TiRWcs^h80Mvw0v^_yK`ax(cJnjJ$1UfcBGT`0_zV+Y) z_xD%6zv^N_u6-+&%YP3G8}Q>~x$ZpH$FPUg=We3j4Y5>_q9s3&sybu*mZLrG()@mi z;V{s}h8?j-jj)e8pV#M-SL9_;!uL&DWHTvRSd1Uwg_t_#Zg?NXH_#FGwW~t`{5XW? zQky*B3Zq!ynt$h#rfJzcd0>qJ%nD6m+h&}({JJqM`ui*64E_@umMGS@UP}%Kld+J@ zoc4^``n7){Ug&JgLHfv@yTBZgo9k&x)5eQaA9v_$SaMfRIUtyMeIHNp1{~dz$ho^w8fY-Bt?XF&V$(+hmr0+^g zkB?fF?hI{B{H3M%vJOfxU@f$v)Lqu3+A-D!z*GguCN8#{vN`lst*{IIpV#CKM{*1@ zTb@P2>Dx>;Kku#Jqq%ga3jtNaF*Bp72-(@~kNcGe)?;LX1Lp_Rje_%eJ$z(>WSF+| zB_?O-t1bJd&5iDN9uQrfj##Y&jnk{y!)q>|&e{yXo1peyN=h1ZpQL==q3qukgef${ zF-|sua#NMO9g^9W7xH>@1h9qDrK|58W_Qz-jCpxUUZ_+BMW~%IG#O@gR)rhXByR!l z?v+PCoh=ndG*1@!D3qIZl^$PCPI3aec9Re`dB+k=#~KN*2JjOi1qXF z5sR3o1S&!9OU@GTAb;-NO1aJUrv-Ew!ewzh!A(cCX7pE!vGeUke_y|QRyx$>ivkWbdV zN{4RXx&`%OfSAFFdt@X~6+xUnN}Ingft#Mn?i2GMHH4WZI&jL}LO_tHJkj zolg9>>nq<=O#kTgQwIfb5fRJHgW2sFcMKZZ_0{8idwO2bi{RbLhtDy{;NqU?=3ZcQ zxPT`{H*2Az?eL8R>P_RO+sMa!6!Vf42l86oDgnPC>>Bp~hJk{eN@!0Ae`Aj?)5Y5)qxjJs8biv-99y3`Sh=N{| zBt1B{vXvB&VUMEs9(ijmV6?NEfE0B8E=VCNDwdhiZEUgerb9XKD(_ggwmKQ1NfhP! z=?4$n_N>7Kvi;GSB_PPtmr{Eh7fM|QH(I+K?8rum+$R5KVS&AUtr$PrK5_IzU4Oit z2VJasPH7lM{{^pDUN1E8dqMk#RLI{uUyYHr>?N>lPfeI(kn#Uf6(2tw*rB{|N{$MI zb>gj9@|)Bng>wJeyr6|Pu;V*cUOf70CFaprH41!((R%m%WeBaER!aeH6=q2_{Y0n% zS^WwLu4JtiwS2uCKbXFkyL|ZgEh&n0d#BCMtctqZQ^GL>Cu**0#u3E zy7Ks;svnqE9Yhu;I`(@bLdNu)UGXNum!w5TJ}=4}GQzF44V=NR{fw9@>|)(FXpD{O z>})wyha7eu3`WJL3W5H*hG8T~CNIx7S5^i-tvyS%Jtl}iqm~`MYH<**UMJJ0E<)vv zD$R#2P8+V2O5A=!mD4{PWSdX`ewp(H0szn72Z^O=J7Dany)F41fYI3F=$0Q`$i}xX zux#wi+^mqbLBDeBV@l!MFnhESjO`+1Gg;tYQO@}cQaAwCwX0hPsJz!Z<>Hr@QSyTq z{p8T8N>lp27MWuYT}aA(-+cSXyR|zYxU$`=63e-<#8a`=HE0N9B%=-t*3VMU0{81*hP$HbM-_rQ0jv_G1Zw>WJ$V1EhV+0=Mxy~95iyA$703g$A;_%zwrBC z&-9N7N_JprOO`4nl%MG-H8&`H6qQ7-(10_U-TyzEj=)X1%nmQ#nOv zZ@|Lg1)Uw5s5C<<2OS8>Py)EfFr*&W>(rdO`+5Kw9oVYF!=%#FI=*xuZiKVDRsPf3hg%hW>BEHeWV`Z#3c{gJo6U};&2Vws zoU{x%?WG}sUc+e`+@3+Q!F-~Ij+k%T*ckUH2>>s5Atb^D1pae3NcLbxIC*)wF4Hd#@k)rXv`C7reuv?7aDM?9 z{B8|4({(*A>epY1nmx`W^JA0qRXyw_U8x?@&k5&B497`*Jg<7D*(R)ToG_te{EEhFaJ^C%Y8wx#30$|n#}oXyBBJ_agp3|GQZw~-+o_EV$g^N zmOc9FpYF_WrhV{Hs8wjC%7>;vIb-wwY8>BBzr+o4e$T5&N(y>;Wr!*TQ&@hezH691 zs7n9x1X3H)OTUveexMBAE{DHoR>=kp9GX&~y1_T#+><=uyN|h{VN3yq%P78b@!WXi z?v0UZfUHbV@E9|BCFh6FAMI|FzkUk1m(Iw^6aJetE^herKy{WcC&$0E4BE9d7+yFx zPhY4fpXQ8U?<_E3>)PIC)NT_T7ZEqSV;D>$)|Ia>iM6 zf|3yrFWOWPn?%juKa$WRX1*+?vSb704*)`gNQ`Z~UB*SmN~1T`3wT{OV1Vdg!^VRa z6dLL+d=az0XG;D?ssQdZg*0yO9|0ZqV4StBTl`f;{)8RCB2md$TqKeBF)o z(M$CXq(A|(QHWot2%4csBsXmaLg2(A+H&&bGhLYiMjHJ`-mVr-47|89cj7;3(bDLAVP)gul(QA|1GD&vuvPF|b z$~mUF#{MU9oc&wj2XfeYf%I`gKp(0~aTr21o*W?`!;U&|gP2AONtxM= zxck%%daR<`KJ4+d^{wf8c9!@aLzaMolC)f%Lu)v$eA4AccRM9fByL`jvVNNa;00~U z&u=lc{y1jFA*1~v+Ec~$Cs4ZA;#F|-Zpx%42>>c1@E`F00x<)jDbVHTldYb2ooA^| zZ>;(7k4sCjDZPvq_ChkueloA%iz}!;%hYY{F|htA>-R*>o_1oHev~$8i(VT%4r>EL z=JF4(nEnd)|?B6v4MqQqH%}jcS z(|75{uwE@Jl!44DA)3PIE5{)FG5le`>uWL3{KpRYPkO_Zw33x&d1qtLyGExS8CClw zi(AL%QA6h^`}$A0d9{~_;^_}U2`00U+2F&Ug6GxmA};-vX|N>=3JwSe28t)k`q4HP z=IdS}=~WG8`;P7a&6^SrquJhs?;KOq+J-?B7F9NrxC3b;j#jzvXLT3)3g`=ykWgc? zg^)FroB{#RlwBVW3L@l4z62YcKxTcn9Z&yD{IJE(S=^8z_GQ(h{8FCDsRfl9mFTeU zo-Sh@Z*Pr0e$uhTZZC{KCH+2)cT;J*#jXA#-OzFTOLxnHroicrwpb4*GHqJ8)w!N^ zj20yP^b^1%nYikv&aClOdG4W`&v<%}Qku+rKyk$yW17>--hTJB{ZamOp_)PSR>N>R zBy9SbIoE7HUFcqj8dLE6N-(mj<3FRYF#%E}V0X#47#;bXB0!TcKxXOF5+<&otbi3Q zg=*7v(SY>ryN}q6ydm&#Q9eFjlmzrb)^U&SOrKeLou($Yl>@%Y|*_CM7HCAApnNhKUJ*n8?lk z9nDySTX!LV!CLw{AHD0z&hY5_pV&5FbTTy&Z+|snf*3+Mf`r1F2uRnX=D~2f$YL^t2@EvV47M6SZ7L0A_8H!L;})Xft{S0XCjP zud|CJ({+&@a0BZIWLW0C~2 zAl7gqJUG7HdmgX4%BQ>PW&weL_wu~krX<$!Rey$8ju17>L__KL$W|Ngq_q|Br)iF6 z+W4bYz?e+o{|+M_+}3sKBjueI2_!O<=f~pFLe)$BAz=B84o}zpWpt+L`Riq?)*CN zAzxbBva9y`G3M;AR#uPRZObi4(9Nvc2k%F9kfT&c!;w`EbcgD zk`M^PtVBkr)Q)wE0ltLUTt*W^MRgL7!i|{;tO87uPjQ0{?6t(9Vy><@0k4)BmQVSS znxWHHcuYzKOblr|q39_xJvqldHtj@?%rd>(l%{MtP0cr2!?8)V%E%?}sUd6%I2r5X zfPznP6jtpj)bW-V8c{b>^)XJA;atZ;=`~e43fLQA)*t$v^6UDVWKg~&FdFb=m9Lt5 zrzyILz5V{spcS99RxSP#84gt4IBvx_H^C9d*0a@>CLhaFA$)XppoWzL9ouD;^Cnf5 zq5D9F*|qIpn}g*t<_tz-N=uvJ;l^7*kPBsmJ?;HEbZ(RN9kI!;YCZ5$iSd3X43c7f zyq|ngGKc#1{C9NP|FWncnTX6|HW-Wk^@h~3IXc>Z%M=t+GP+n;gsdi~FrArG9oK$8 zMGm!p__%=eox1zsi}Zk*Y55I5dk{kKyzYl@^VWgZCxeQ zVNNIYHOi=>uqVCM9*!#b(N&;k_ z8feJ3^vl`%<+m#kNDgBux6=Asr5NCmYw<7DsLvx_NL+RG{{hSj#xw$xblKkR)Rp>i zcR!#(qqLN14w(x#W43(Jj~cNF#SmG8J&gYl})RwY67^QH5A!s2HfoM1>wk*%FZ zl+nIB6%QtZiVm&?WmP})zdDAS&A$n11UY%0z)aF?yQ_R_2wEC%=ghz_hPZ;#@PNlSa`U@W{aT(U&a2;8aNSry`F8Cp<;-MwFS6{e zgQMtUKe^8zGr@FQpUSfWsEfP?Z7}+=txbiQQRYTDKUo0!K8tlfy(H6nHx;h&d04{t zXy5*Ggf$j3;Qz}nVjM$Y2Q2tN32KjCa@63{+I8RjrS4e(Pj1}fo`O27nj9buo{WwH z0!TpgUOu%acqbUB=(qw`_|E4N6-(eMXTZX_SQ0tKT$_`VXZl#?SseyDFRwj_XlBB8 zbjQIlZslFePw?ybK@Td$zFys*xwSPgi#3IcYAT9Z`cp5OVnB!dN~_2AU5d=Vz%L-g6pguba7c1n7_^&s|h)r z%O8U#jRD2rRX&s&*thqRg;`j2Ix`c9=lm8`U88)bE9W4m?7|DxdJ>lw$+3qcocW)<1DzBb23j}i5~+tH6r}rmPrkoc zTsSH*hWxU7+?6Y7o~p94vCYY9TpT6(5-7HjICWRjG7~FWUp^EOs5nBI2qspWnpl$a zlaIjomhvvub8xbVj+s=%pgf2Obw_5_Vk+e&or1h_vb_Xo3ZGF6%$2rXBzQpn6=gRmp&z5OgFttfVNuTCFyW9 z`T+l8L%w<(m49-$I?F~Ud7M0dei#h@F1f*N(43pM^R`s?;kN!QRAr)^!_8Xm=d|ET zWiG0`od0<@d9xBP4zRue?2jI=FYLEC%||jvN*bgTANp%O(ug~316_F#DdJmU-B2JmVV)BQf?YgbBga0jaVQrQG= ziw>rk7fN!de9`e{LUF^X^>pC4lVlgKFne%mJs;=z&OcfyP44clo<~m)Zh262G#ak6 z&#fAyzvh~ae-G7)4ec}xIALHsICd&k#K;gdOjTWNSjT6>AA-?{+3H{|%Qkn8dYwj|lrm1mJDr>;orV(c%0 zUQw*%#REZLv`W*sI+`Xgx2RN+>KQDcOTkNE$0z4XS4#pPsH9vl)z788XL~f04 z6xG!7w;UBVN0bwy2?4!!&)FMv-TVao=)+Qxs=Czi4&`_LLBIBkqY~Zbz8fuKA`->bGO&;{&VCdBaUq+& z_C6pZVZ1+Dl!gb%qcnf}ZX=et?B`22Art7w{r`RmeKPm-zl&Q8fZrNWNu3svTDHvZ z&Z9h^k#e`GPP3$3snq0v%2h>s%KF2^6YO1LtLr<=*gw5PpGRE9`O>IyB4dI1< zVr9*!Ij)kFOnbM@x;9gt`l~Ke3PFvD-Nx~8_a$)>xC5twloGP3p;k0UzGHZY!@CjP{} z@W{NtwJnxoynkEHRGIRxQ^=1ooVmqPfzE*AOIlmKcV+%iy>lsSa>-~VVYQ+hz@;F>RF82X1tMr`nrtUL*zd}`s6S%wLs z9Oojio=>5;*-MB`uy#nVs3G~@hyYpK*Kg(?*AQ4A@h}sU8zbfKDJn=eIezvsnFi`| zY}k{ulY_vg>6>hAeLaMv0A3oyNhvOXlO3<{Y3@n+WIg1F+zT1F-K80Q8|Ru~Bn!qO z5dT!lxeCa~XFeikCNA0MsWNKVgyaPP@K=bYDSv6?VRQZRDQ7ZO)J=dUb9OVUugGXq|{fexH+Utlb4 zivZO#Ct9!zNEKz^!GUy@?gFLBnZiQ|JvCKU2Kg$!l_hPVA4N+2#y|Ba;erpRIrhUpfyfO_ybKVpIWgqbsh zbSLJkqmd>MeTjEzH4`+h{C@>fpx66odB^RyKA!+vG1;;P-2tww#(@pr0(YTkd$)dzZM1 z_0NJZ(*$MhT&8C4T%YzX8X%ffw?$xkK1%S;b0GYmG?9#9kf#vf5fAf!bp;MaaDo!F z#AO~Wfxa}{_uKp~bU^G`mgtSt85(+QTAN+uxcD*PK}MRz`H9>+MUO1n(nd^EPaP;JTc zoj$MDRnh$u73oRo^99r&GrHT(0C~_V9*EO=(UxF2I%pHz-7qS@)OTZ z`sRjW?PknV1s}PI-QI>XuaB&|2$U*t+7{M)+A#W!iyP%nx{y4z?G&?_9ctmPFJM1- zAwK?pRknDbZKJ$t1q^iO4QUaR>*ohspHBHAmu5{?X&LIctfJPm%;sb_l$A*KR?WQQ zk|8BNzy|j6yU~HWD7kw~=_b!>>hxGZ<>}O8=h`~`p%aq;m|Q-u%mXMOB}7LE< zsvMOk554vY!O}|XO$oTUIbN+sg)h#p!{XL)>Y=zc-$Y@!~AbK0BNQ5vx7mywY1YJ6)@C*Fk}7gXNLO* z8n_J+Sv3T%m59d_k$&&2xUiYxsg_T5eQ9W;FR~FULe3gg9rsmF*eiBBT0{xpLb?6c zT#i-iwK$FYM)#HmB6X_0*!OMH1LkW-qCU79VeY_XEuD-85mDOJrDf|679aFNi z{a*VfJi7G-3L9m1bIA4l48?TWJLc20&%;Imd=f7Cclab{i@X)G=URSNniXA~qXfge&wy2xaNb!TT zaKs4@QRf4YQdkw@6he@~uEunb#pq);9snc5b5}^}wtNwmI96eKyM>??mx(Po;jqbF zex&cCDY==A$?qbkWHEo758k1 z{~YZDGAqzYwogEiyzI%qNps9cj2xoj0#+;IQFFPtcLu}~S6yeY0@CIcg=ys!DU^t} z6yAPy)xkjvVFICrh&S1<)Mg}n*2?8w0?+(fPVyD)?Y%E2lXkYrCk9dGIub5XfSZy$ zA1D*@`86^6v9=oQ601nSk^19x6sw{5J(c4IqfY`d}TJh5#%jqRjqW7~Gp*ftycp7$H~51g^jUTZJUxo}<& zeb=TofMQkj9~Ka(Vj&_K4H(w%LE>dFTT30hJEnf^e&lB);QwvS?$L?aSgNf&H@~WF zX;}{xaztnb)W{|ZUVysrKhpIKU;P>)sd7-M8%d+T!T7Av(Cc#{5WSabrwxbAnY*wWQn{>?L=AcN-=4HQ^Nc;faI z=6L{e%{Y0AK(u7$zX8k%+x4}>?*h$V0{>k&0NyjjUA2C{a$yTh)k}J7q^+4R?#@}Y zXRBrbwR)x+cx-n8p!sKQP#Pn$6Se$LEXX8YV640>j}kXF^#WMw)=m6()BQJU2#T07 zi``mVM$=Fj9h#gadn1sI(*tkZ6Sb3nqblSFPs;A@&0Z^Yymh-4GRODS#&1d*Dav@HLLB>g~m%nxLs0mrRJ!bB-E@pudYj(WOaGT_$OgxT=A=1 ze2l|?))dg$3c1n_h#xY1e*TQ}h3X*%4fLnM!k4S9@}QkesL)QLyT|+ML(Wbx2hOH& z#T-%|j{Cl?%Il6qWwZ&b5ZXsaV4PVp*CPmlO7U=RVijcDx~OKQ6@rIf4zi@aTVJiS zW*T#Bx5jF)MnBGHy@}AO)+7E84A)A~c_2l(N1zHkA!oo!U6%2BIP+(7Z*F}wy&j*qS#&kB;xbx5Ww70hsUd03wlXn93+d?% zvf7!?St$rU5)40FG09KGoh8UZfpCDLLAw=ca2;i}I556SswF{y5NcLZ2oSRkEyi^U_^-|Hv zs`HuhwMC;|S&19Nv*`YQr5X*0Wk~i1ReAoYwFoRvDg&KonNu*;E0^02_2Y+?Sj6eRg#=E@gJL)qKZD2cl9ern%!J_U@r&`2ju1gKJ`bWMtt_ zCihx>CAj#5cU3x3{6E>>%(a%uMT-1`8AXz{^mNVD`cB2gRRv)7J6+P?*Wm3x;AVas z8&zQiGAj9c)@L!nh>!qd=rV^JmmTOBVZwmYO$0&^ydN+D)hY^ZiC!-DJ8hIx5!fIw zLGLD8N_0A-3-j30v6{U(UT?3Kv#HLijt#aG7k!$8KURf(8MlSMh^9LY!mLs^W}mc& zQlCVVKZeDRXrpp}{J7#tw7uG0;C=caVg9 z!;7y6{e@mVJ!7Fo2`ZeOppxP00ov89Tr*eJ9aMXcK!(Ku0Lm@)`t~n%_sGSqtCeMd zaA_DAR^IOo_H<>5+R$k3&Rdhr<5d{GSW<@SBlsUB5a?xa>v#Qd#CrpV+&3BV@k7I#WD=aICGk zD44V3a-pg+RFq1AI?%N;YumGh`{BhW^aIPHqN%=K-2H7XgN)HMOXqhH8R+GJRFc^?XYV{B6y&bggMoc&%eBe? zJj&PI`b&5KIh4K+Flo#(Y@>WD&zfTVcz9odbN;85y%8@K-$_JV`@iP7m`YN62t6bp&gC%$9$w-E^G zC@h8-#Vv9Ce5=Iop?{01qXdHTkujj6T99^|0`PfLQC2)7qVDUY#0$w~$V=MEJUe(d zo!!g)Cy?wtfBsr)J-Ap{XA)Y!c`$dkb2snm-m>>YuE=`I79+u;QlUw@SNozOG~|7y zVID)Gm-cqYP9_=8>Uu-!(RwJ)Ht~FQW81t%)WECluma8p;MlD839oD5)IRx`7m<({ zL%u_%%)jZM&$(vsxX0}=b<2@tA!czA8lpY%T~lDm)-aH){Qc`%+LBd3m9pDPwN-{i zVSK?iG!}TqWz%)bnESdcIjQii5&f>@| z+sh(qe;{HOQ4`jv{*TY@-Zb;fmOc5zVZD|$;V-^Lc=Cj=3deY7z??ozz)_$U*43Zp z`sY{|y{g!F=R;u6MuyzqoyAZR5;5M*OKqW*c-mJ&nzm39qEe-?#)HJ9;}fIFFhrp< z<&~hw4cA&UxI@5pcf7GKG3vsSHLY*K*&4sl%Eq0* z2LGc}IHSFp zrE+H{19$!t#rKnUK^E4&jQ((7;g!+({R@W^HW#uF{1e1C#o+DOpJe!%K_cOw`e*DZ z_u8>CwGn!ibzW-7k5{XadVgGtdqiSzHlRr$fdSavZKik9R5PeUm@<7TPL2gaEN&Y7 z1(e{d4Rj%kvVe!pf{Qc6vYT-D6{W zkdnfT3Pri!QwOk;P03&_!-zU=r&7C?Y>b(vCmnAF;G9%cj53xv?!LSuU1QE~qSCuP zoY}v0Q@8|&U3SM;?IpCjL}o<70sC;PY{}5z7OM@WyOC{8ucPyfZVwr@=gH0;V{mf9 z&9u0b{~dc19oTs-OHGfbPs`()_73-lt$kXDSQ&QP;%~Ea{dKr z+ztKK@E9KuCpU!1#cyB>L=y9-m5t2H<6$NPbb&vu?JM_WwJ2AUQ{@yV- zY62k+M%2~Gh1vVe%+Jh5!6ser+Gzy^rNToq5&dyra3E6`XM%;%y_gW|7SD{Zp4b4svKDuG zcH7S6Y3Iu)N5+o(Mc1<_<+B7x_51sb#eg*G;Y%rE4NjDosvd@re1C_8+)e#`hPs4j zI?~uYp23ksGS$PQh295~@q40#@3sDT901Ymw4DoB5j>qpnmmWqI^~;)Qm6Xf{GaNd zf1l4wbnnUt_6W?)&7b?Q{d4kixiEbl%wuo}!;7o^11$2?HZLhp4B4<58}d?05Op;! zTkh|bKbIkCw>;U%4U68Qh=jG+2}MXvSv$k@;P0=g&>qGb?4vpAW8RO?BNsV{iGO52u-ppiB~-V{UH+AQwGE z2q8{o_5PX*RG`3J*k62fIX%~cUpfD|<*IkT;e0gg@$p=pq#Z`TxJ~ zb=0)>9Vf?vF{f0So)HV0$JXibVG1!vhr!OycVVAD&MEa(Z=2T0vll{Y)P`qr#bkIy zK~A@M+Ovp58vG0zM&7UbOIQTldl=gK&l)timzP)jXj$XQr4UvYp&1 zi#m$#u=#OZyT@CtWYDw@iGxkn`ofKy?0S7#3_QZSkH`|9ZQ+BvmAW_Mu0QUA12+BXA!F$V#2IG<}0$Zt*PgBR0{(id3~G;W*u2sn7N~z;rrsNslzv zy|PE*SoVX!^xHvs1H*&jKCKJP38OSxa?QxeWX)Vor7#|D!J$J_mVve>-Vc0zgu=Eg zDb{9W%NsBEC;8d7AGjBZ&un#CXs!1~1~9gfM)z;7>dX92fybWC-gz5Mk{Tw-kO3U6 zbgA-0@n5;PSSbFIO{^9IiO*HxeM<0TcMn4;nhXNMl7z`RzrU0z_ZO=ihjS-2@Ig8M~jl0-kQuahTAEn%dmb{+-3@*XS!GE&M6y*Zj&mH3ho^QM`Bp8k{L5<;e8J zbzA|-Pkw%YD%%{fGJ$9)k{Oz?EqG>2EMo(P+2cp(i51}Pj2F*Mqgv=0c?DHQu`S`Ges&8&NmL*w-%m0>sP~7MX-P6%~r=b?KD@*L|Foxs<%h_lk zAM_x%w*%iJr2d)=M4%F?l2A+kcIQy!e-4>_$N;TP$jQ@`wM7ufuFcH!YjQRkGKbT; z+`D3o@^?U(=9%R7QycM1TTZ@4ViB6db1l{1c;8NMZ2%RDU@KW_j5|bG{a%|xAVR4?-eW4kTLkKI*ikOmCAve%ct3? z%xQzrN*=sZ>7lu*pCLH>`ce${t|MfAQq3gTW|Wdbt@w!D`6u>b`mh(9h+KLQ=Yn*DcqXv8}nE4^#~wKH$*d&o!;oyK{O!p0j2m zIG?ZsL?yUhZqCPjl(v_2ydi&zPcm_t3KmbVib%unTzN_=U#lI#pN^39+@*o&5%k7C zCamyi)oLHN6}=&GfbNIlk>WrxZm!z7Bow^mTYXx6j&<8HNy~s2<55a|%c) zy5DS+ACcMhvL2PTnUsF2Q%!J)ehtvcIxx?e13rmymInod}6K+cfW)j zGkU4k%B|EQb3rcvB@L2nHwS3--5TFDFX^9eNo0UFi;aj*A!A_mK*u>DC?aa=2ts4p zw=M3k{O2o;5IYjdU&$n57=uxQ5YM2OJ3$r>K^Q^Wt#WE<;1?JnJ+9eqaGmI;M}=eR zg27Qbd8E2b9)kYKf&Lhzm(@x5cfC&~v;lNI_TYI? zev2+P+y$>J_H)aZ`C*swg3}hP5_r8Z{?9aCOJBc0H|CoRc;NTd(R-=DJzQ*vvL?dX zxK(TXN&fdNzR<@L2ql8-CNF<%e%QG^vu+5nC1jTBy}`SvkeGJ3PVTv_sC3f(#|1S* z^bwVZooI|imAmt^H1V=p&DefZa+dO^(YCZ|?zU&4CyoT3*W}E(EjS5es+kfaMzW~x zm*X*UMZtSWL(|{UsC%|B@|#s5{Ert!_O1rk9xlU5A)DnWKQ=6eSytMysShgtdWX)n zAEE-GTwYO=y^-UH1IkB@5<#*s=D`d6?9K4cC&4fO&l8+YF6sH$tvESxxG~O6wUOf@ zjD|Lb8zPnvN!kGoF@$3i3^B`u^R;Of)B_3Avv10(NiIztKo1ufN{*p$qTpUO?%mwn z?CGZL-s!KpBzI+fW!L6z?q;sG6)#cG%ekS#yP?+YmFsotd8#y1uoE2sg8X6U&c3zgh+zRG}UAzVI;(1w!^V@_Shx_3ji97_&t?nh{ zwp~4RT)1BnA&8&^f<{%{(Hr#&W5xLQodk&} z3#aP=3v8I=NYOSCtVecEYDO*ir%jU7``D2=In{XecWHz0nG4lu{7}AOIy*Rxh&(Yu zO?PT1-EG;{FdA#>s6mCZ#(nE!TQpPv{iW$!dHseT)6glAea3~o&e!Kn8-EZBzJ4oEeSnqjEJqP=^~ z$zxd}G^e#hIECcLW=Z4bH@a+M;kKB8Alhh?QIj)xuF~T5u(uF?f zs+O26yDo-*H!HJB5@F#d*(?xS)$&mNB8=LhTrvR8#`U2<(l&Z1roFqjWmgb(0%8hA z9Rh)Qu(yqcgO{jpz<#BP>Q|A}1Ntm%kX>z%&C=gTLvtmnK6ua0h}OOcPrP)@1EKC8 zJxmziQhv+y)4K(Xd_~Thjt)?cYHCtf*lNSg!F9cK3pXJsReaKQToDI2Jb;0MYRk#( z*&>(8Y^{_B`4A0iF+Hz(djXku9F-=cz&>6JY9ZDD?-V>vWxY->8Y zAwTm)2XON?qV*?**zhNL=9aMO{9oTZZ#5gu2}4DC;oACHz}gZl_J$BEwCZWqiBAN* z=E=PazEhfze59`Qr`Sk_M%1bNCaL(x97}y{G$cu;&L;Y!1g9jKp^7ia^?TNKiMEIp z6+9l!$yGf4ao1J7L$6ONqaN3x4^nu);nIXkrC#=Z1)K5xLBYp|vTum>L+$oDAc4!w z_kyUVvVZJ_e%T2U*mn}9IQ3~P+Q_q+=f{@UCa#yw4wr}LQVkdX-11jjjQHz$cy-$} z!uLWjxQ@>2jR{Y@iB616&{Z4bBey;Evk7pTl)SAB9c{$20WwQslY&RogLY&{K8*uw z*})LjXTH|iSxwyCT-U{nUt)8St^JM!VvsrGb*|%9E)C7Yr*tO;B>8Rn%&5KNN25o# zjz7m@ls~av8v5b9Y-_C`$gW`iYA4B;{ka;#BFP&~$4EjI7;ggImj3;{z#49{`eWos ztHA~;8k;F9Wy#mYqou8u{m1$Rnls`%j(N5-mKUO9kiIwH!~}=ajD8pNsF^CMu({<3 zMqot1V&+twn}Sw7nxpUX4j1DhX&#FSG@S;0G{rmGD(b zgZ-Kk8Ss-t;?gtYk|93dK|C598#Xd5tjs7@yM{~Soc27iE6zvxpa#Lk9ZOZQkf}vV z^=kb{qyaW~;Nqv;>ZBrIi~47G-9`DN%Ng5>B)EjD`GaUs&|9ev>sLl?NN5mHjh>ck zHI*ul_+IH%>jqbjkcOpEP=CsBUChU_bCeXqd2Pd2AAVO~%S|+_6aa5xS-U8wohO&X z{qIwFNfI+;yN?DV=97z!w5mFFM|As6#W2vM)p?-G)~CMPfKx9Ng1D2QGNXmeMs`jP zN!}lz4w2q-uX1Ht%Sw)}BNjZpnM$<}6vQs53e^8@j(zB06aA$#rieY%CwF2*s^VLw z6H3WQOiV&#tyv||?Wxq^B+=h&T_cQRMF~`+OPifv$M6ig#Dq7e|0QBjFZ<tsAY+OqX*9GVm7_=I9^g<=5Z7WnM`fu$Zn}u^5w-CYLh{yEo5*!Rb{08 zD?kA=ih?Qz!kWw)fAe%U@m!a<*zdw4VnJm+ahVU`(g{6>9Wr!;5RU!tq!qHKf!jb& zpa_=i{ap=JF6*BwaFrv&QLy-n)of=+QyZ5K$?}pGFaTgzlHBWOx*U(*y1u#DB0-!~#8vIdJR!KsZMmlUo|4pUo^*vD; z|89KzuP2f0DKv=Xf{vMONjn&}RSufaQCr=x0PY&?) zaeiO-{FsnY8?MsK4^!$Qg7J=(cxx9k#iOz2W>!+)Tb`&*hmKyeFL2n}_PAOE4SaE#Zl7;o$@$HG!V`4#-2 ze?!|b`~p}bxHeXZsQw$d{#!mCiwV=qb?(GJRbcydS}vhYRgtj1y^NU=mfJ=#!?SQN z7(0-@r>%k>acGee=B6RD1tenSqwc1t9U_5%Uv{DrOqZiKXu}a}hjkApBUSHNTjj~% zbXQ>KbidxzrBq>Y_ZG-9%WvkqwDJ=l)pnNy1B9FEfD%a>^vnC1FPNX7{O4#C`@r}S z4F-H7Ojul;+u>wJFc*tEmoW)xLlb!hV374=`lMRrN{ugYh;Sm>?#y|+VAcccr^+w@ zfkY3H8%3!7D*3$LiGm!WDH$=VPjnuL?&*WNJ+ZXUYB<~4@hBxFWiLaU$l#}Y!1Kz6 zk4(MBne)H1%vO2o@!Lj?alI7tjdBPQXk2y*-a>xYC(>qHUz0WW@R$>!l~EcoCB3lV z$fKbLj8Z9JJ?LPXK|nuDHZI*^x+PCh zPCoo3Aey~(m)au|@Cjp^&1@HBbcx-#c1@!HOyqHb}I%E zDAx{(Y6X^+lmNBmZ-UuCG>uSFaIGnWVaD>x#%)_%h4o4{2|6;9P5V+cmf6ojGXU#F zic2%B%Hmvn8(V4-0L$Aew_@w?VMYMnwM)r{-+~K9JEKdtx zGz|;KJwFkqK&us4r^C1|Jg|e#@9fM;jdOdG_JAQkg30HoWS9gtgOR-yurRo&w5drU zBeUT!@n%EY4JOOE&I8ZjEP5^INNN<->*II)Tp!>M(I(Hr&n{9LFI`i6@j zt`_pWvAiTBquv~RHfXJ3u{2#~z~r3Ev7(*#w0Iqf8T{?rSYX`gE@Y=)%^)HXxwR8? z<;m40B|le$Si>BYaqF4Te;!}>dmdc3>I=3sYd0Nonve1lp zYdMe?VG~BQE^e1-H8>z8-YXTG7D!&F(v%!uokVKKK*=RJr=MDjwp>`d1_MeVVq{&s zB{RyyR2?3bFm8^^=hLG3d~RA;)hK~JeNe^G;NC#CTY7{fX%jR>*7ij|4C?l9fG76K z(~QX%lixX4(qB=rAw-!1s5Lw>RJ|@?0ymGarsWq%Q;t_I(RSrY!0S=7Y&7oLJ6Jp8 zyH!@yNr-BZ$4lTii??_1Z*Kb_eRctrjXynzFK%vW|*CVG#VRp52#*=4KK`+3bGQc!$pk#Gpl&re^3* z3R z?}zP$X9F@xao>NA9_tAEZV6gxG$>sck&%6UxEDtZgZR4^ z|78pA{qlc*O%=3)^-sD`qCHrPc&#hPhf!k@Pb*u$JUKy*vIu&fivX!IIENY!zW>l2 zb!KL9RZfK#liOweu_MXcdtRF?64F;PF8wrW_O%N760*&4RVl3{S~+hy_kqw*utWsb zEQtumaQk{^_vPUxWTS1nurQf4L#~actsfOLF<%s4n8|ClfigvIc5xML#cNU6aQ$Xi z3wUo*AxE>`$3Ll6vh<@df04%}@C1hH8E zLdX|OlxgL=M3Pi(So{Xxq10gERxI`H-)M1RNeTB0w{LfYt8LIU`h%R{phP|?sl8IO4vN}9512$T$d{5^XW#gVQh??XCzMhL#<16yIQ*L`la}>@eJA%IASe!p76XYvw3GXlZD(G&{>MfF#g$d{Bj;)FflB!ueDqJUwtDEU1au62vLy66GaAPDTH%=wb z9eFhu+`I1bH8V@f`X$|S(c=bO+9Pmj{x(eStk={fZV*m8vz^XmsFw}{uhLBo zDGRhmPVQU^QI!D^Q$@G^u;>i_V1-iTsstMAEe5iQ7ytQI<Yx20IfC-+N! zeX1KJ1%iQx!Dt&873x!^ZLI8cPDq6re270SoQ6%YV(QyI-^V{BiG8&TaZw`=ByaC; z9y@sm;sU7mpg@d*Z;IHL2c%O5nr1sy27u<-f*m=Sv1?A$NXQ2Cm7%9HOO^q2P( zh3Ysq7@03d`aI#A#K3DU1k5o|n@f?xxaEP63K_G(s^oy8Iz}3Xt4+MfC-KIpj5jz` zg&bJI0Q_1(?Bo zz_DSMesBrf^AQbW*wzvbw91%prHC} zQ4x#Ih(nfO;MGHzwRGp@Lckb*CtzND))Ry4ah2l+ZV+LuRv;Ah!4mZ( z2O}PjgQ{j%hv~QwHg?ic2yNuh!I)yF;?tKD=L~$d;*Xp(#`uDq`JQJ)F$&Yfn-VXI z(!Z<==rZey?f@!?3)Ka_AGx~IVD4fW;k(v^snREb=~5XxzHtjbNbR}x7>%69KXoHF zEY_HH@0@cYKA?Tp+YQrezxOJyo969d?B{W%KPM{cm{k?<8i4PZ>e;`e?8`y)9p}<( z`@ScPi;BAggk6u#VEiywQd?n@6ev@WnHejK3hZGJ^8PWh=c&)qNBM?3_1j&}qQU`$ zxc@To;#qUG{^*M)tW2p4_XFMsvCAG{{4r)Si-gTcL`Aw_Xs z3DAW|lJnZb8Q9Lk=0bIh z7>2Kej}2!2r5jR#_|XejGV5FBK^7-q)wh8-kUAQ+-h9e%;WwrY&xPs~pD$fj zla2938wY(ZFAM{`&sE5g0~a15U7t2cl{9ebA%W=Fd?~P>@8dX$@9Em@()-xa?R<5( zS5$Ho`=8vr9>B}d=;~JIc56=5dPLNL1G>gx@rU<-Gxx4Qzl9!%Esh0Dbvw!v_l$JR z*~;9kunLF{$Y=gL3eH*(%EMN&@Ro9n*3VJ$lVd2& zbF^F-pRZ3SLl>8jonVed7A8qO^jSX89(vdHdQDaMa&VSvU-QvXr+99^{s&5PTv*VU zU7d?bn~rsS=QVPl=Y7zB@ZCD+RBWudy%~OLp7zPD_S@4b{#*LsL0WMA?qL=M3c$Q8 z>fugDD`%|h6m~2!-zy5Z4ZQk-Ky#t{YaGNYz;zi@sq@so)Dd)~_wKV;rEBeW-@@T0 zNr>)NDN8!KJ%jT2cGc-~g#tmPF~;AIqoC_D3TYK@ONKU={0hX z9LL%keNGLk_M+&jgx-AiVSU1j@M3DmQg&w&O?-~8J_DJ2FR}(b5_y`3sQMth0*)ZM z^LCrKXoc>66U2FmG1s3Bv*grgK-bLmYn~k~*9vq!27i{m&X53OO5^LTw?b*qG8UD* z*3!}=^5CV3a;azjZz>H zM?}EUGvD1c*}BuXZ9`LIt?QY2jW`uVS{b^VBnK{fXBEe2Dpr$VZE*AZ0`K#GGJA(X z!V2cYPk-;pGmhftxFg2HSNl^q%)|&XPJM2xKueU}!GjZijHKnqg z!79uXE(|CT`KIhzEPWBl-ZD$=ckAKZxql%z*$Q)7en zj4FvMIksLdkv&dis}9oK-@N2dpP-xL2wY`m|6%PoC^B97UcGvp{LvpSxa^N@Qx`ym zk`wvOt+Ih4dEkKca@Pa@mp&glcCsoH^T-F)EBHI;f*IRK7{K!Er z%mJg_+S>f{Ju)P^CAj~X4}}1n{wfd`LAJzN9i79=Z*9iTO5*4lo^E) zI=xBcsso^dFnMelmSz7;;U_TujBOwz(%{_9OB)uk4T_ZvGT6GuXYBqb&iC--1`kzD zX{^78+w%U4A-PVz6!dr#XT1&C>4-W(?@e?KCwIUh9*RCL^+b|C(9)4K#)tM>by<3J z*=aguQIq%A=JVOs_5Q;}w%?2S;Y2P+Q>Vf*Au2A}qRdT6wNK7}(&5HBr>ibbkCt1{ zLlZWTS^XG*d@nbyq_zV!nhc_Ci>K?cz6LFortt1&1mVBqVTWol0o3&0DK9{S^v)Ee zzl+R-VY0h4mpf3@r?3v)g(Q=39VF@;Tp%Cr`isIj9*JkpiK!dKaJ;Kz5@aY^w>zAl zR?`H==SpO+;i1wZhEOwVviK8aZg%`%EClYW^UMqlYwASo1erW?7vQt7`f#H|Q|dzp zb7nTb(myiHJQE+%(1Y7rKMp%EWPpmYW zCa_^;j};&NjXwkZPmhljP5LbOv;k9xO|(NVypg$N{#5P?o*l$|RH_)-wGnlt=XU;~ zH?ozTmx!|i3ac>KFBvb!6lI%D7h1vY4{v%+uC4iGB!|LFF5DecmfWKoWa4M8CEs)O z2vtV2KVJ&Jk(3JRH7I*LW9|Io=nKL%E(p=SzTj{WA-2%{iKmWlTZ=cBD z^G*jxPhJl-su=thpq!taA1E4}x#ni+_b=k`l0S3P<)Fefs{nL%DDQK)B`{S##e>c_ zt#ulgq}~~D&b9ifDZc46)%F^7dFAG1V4W^L`&!kajd!~t#!js_Ni6qOMejhsO z_N%_}xlJ970_r(ZBq#=(-3INXnEsEuAH7S^e-`HPr^U{o(9+UE^h( z?{{W(I3ePl01FFcbhdLhC#+oqh~Q^By~7sNR6clWD{L`ZO$~y;ow3J#YtY=bCmyEQ zRm9?PL*FzlWGt@Vm@GQ6e%>7ic}jv!Se{_TbI+teJkcV?+gLTGT z*7YnErLO44QYqDUj{YWyO!v?AqpC9YlOAb-NJdO7SK=p#ZLEnd*CW4bXWeA)cax%( z_JC<{>`;AVK}p_%o;L-LSLba514Qg>((Q~yAsU&2Cej3Wzk3X013J<9;Vbkd%YUAl zf(9P5<$Z5%9>0+RkQ(FXC^I|X#71{#IKs4GTC20U;xbSM#;1#lJf!@1F3Q^#?0&+8 ze;zgStA8)Ag!_0ReVYHXGiV{A3^~0-9z~No5tUIagqDwhw!Cp5|MOx%4Fp;M!9%5Q z(dgDoYYHkJ6NOyvNZ*%Sw~+SS2H5s60+~2JP|G03w|lVNwjMGMh-N$tbg2SvFU?Qm-F}?UK1Rwqf0NTY_tNY`h`TVS^ys#QNin)v4s+|@N2~IvgwXJ8#XFZ% z#<~@4{eK#kZmvKwZIVx11b9CPfA>S=Sc8R{fM)EiD2pHB$tt1*5=DcGKkp1Z2H^5? z$`ePfeOm?)SH7q&-$TVOA&sd@$_jtc%|Jo%5ai&yqQK`iCaCt^ey2{I3W0e5h1EHJ z^wl2ud#n`^oL%e$Auttlrx2LTa^-5Jub+P##o*j@0l15~2M(-QVdNul?Fl3Xq<8j-3dNAA(}C5UU{(A&Qg8*|2)SElMwbo9OJdK~ ztH7*Dy41DLM4VSec7jr(vT|p>TI}UASpszNd@nfl7H*E0&qo-Kzj&4W&5gu$DA4a7 z2o6>?K9y?ktaQ=f&71nl44~}2t7dZNDbl?5Ba4Lk7%j?w_|tCPs`07JV%q-`qtAoV zsML7-2#|ikNOB-kH~Jd=*-KB$rBu!KUyTXe=8GTPgyGNG^E0gU`%YN}7Ga*jByn61 zN~iAEA!W{B#g4<3Z^YgY2W+7lcBp>s8h`ouT46o5miV5w`*q!BpXv45Oy!bmOkU9S z&2b3`_Q$Y0VlH^y7nFH71<3h_>l_Ln<# z<4hJ0=URi6B7n}~K6OnS@(jOzT=oNj8&kkj>x%exgZOo;U=X$~Dqmhw`?8`0O?PYt zxp%J}T0}>#zO}^(PHYCVL4(8y*0%rS5Qwq_^8>O}W{tlmciF;O6-eIiLlN)%sIIjq z?%}56#`_qjbGo6s?kF`mB@+@{d1zP=6c~kF;){LI$t8U>c9z|&8erLULjG`46Y>38 z@ucsf`9?}`!add$-h5LC(id`8fiIaKMRs9lZz}ctU71tubMVLK+~5OqI@*9ko`%Z| z%;!0Nr$dePkT2;{@sK>=n>^mXUh<5wJ1W!y3b3>a6^~rNz1F^{c<^k6l@s3}7@{0` zRsXN{+|;iN>w{FUNA8BQZWX-~ZO?986{NtxV)3R3`h{}?onVNrN1+?KClGoF2}B*c z1;LqRFB9LNxu{W*+}!^r%Q=lwVCVZA!{vnwgRl8caZ8O@~Z( zU@>Z%?D7xU6J~uK{KEa}?C5=Lx~C!|t1UB8{1Fu!)rC4496ppRK>g}caMYU4a{67) z2wRd62oe2IVxr%}d+2xDRs)?i@MU)!#3`Myex;{l3zoIyGn%J$P+2nZJq>*QgGi6i zZXiJ7uqm^1Lo``sa-#scH5U9BK$;SGrB|=gJzyP`SBZksKtXaL-dSCGL-l^RbjmhPjr1w^@Eo(RhkCu)UpbX0{q8TX zPH z8wC&HziDAdKD~RjWF;cZN~ownLw5A)@`EBrFNd1pBN+;Yb4DifOj*Gw)XSow|am5Bne%wYDyb?Mdf+p!U0nP6zr z_#=b`xVPi;W*I?qI@dkykeMsT_4@YF$k`!W;EL^aVp@+?V0t)PgBrw{7xt`_vgjXN z+zQ?xC+mcw_#px9rK&0EhPAO|~( zmJTf}9KfLWy1LylZs#o?dmr`c5M!rKXzGbpEy(O2B%~l^kG@XPpk~xVl*aV@Z8)e4 z!4{}!vbeA@;SuG?_*gCph7iaE5gl1;XLl>*zngMhXCYmC=uxr}gs!Bmh^;yXc_+n^ zZ!u@V^sx^zGN-G^C0l&;M~M#pl67xWVw=4oEc)MXr2@qW1ey`(EEQ$8yB!FvjwB6HK!+#L)|T5&vZ zi)}Mt_q$f3B^$IoTD!$Za0CvTnoX1^a8`&Aqu8irY{BVZBN)8u*$}GZ5NgFrBl0Ro zV)5((Jz;CdV7J_eF(JXZpV)QPM5DvZyTq8eq5sxqAsyfW_ zJS~IrMd4&h%_T$h8qrF6u^Ti9Dc&r&he+-*4CiEq1BMx5hT1TSa zm5Unbo-cuo384pb6K?3RN}i4 z#Bw@Oqh9)NlQ4Ggi(oNneN{vH=Aw$d^>~6$aCjwfZPaw-@sn$OigacLJnvm}xq*70 zFf+(o#KynQd)Ap2152a)!_;Nz*61(k;H=Rk1Pqx)5(1|=-8HY=e!B_0gRrAYupz0) z=a>2`zC~9lifxd6$UTDm*eFfxauetT&vGte4ig~`|C5_kL>6Aap~QGJ#-l$A`UM91 zX*;~y#Knc3`u%dCW0smC!RnwC-@kXV`2&7B+9C&p`1yg?Vkn9dUg+u?ysZ#&?-4jU zl1?q&tugn6iK?hJylgOo2Qu>cZ|R+f0BT~=VDXwnNETi9 zQQ70B9*Rlh)%A9-QC19FFPO((9c9*S)D1P3|6H8MaO$isUtCI$Q zvEK2WW+d|W6!$5XQdD38$;Xr8M@(*RQR1L3GwOwXE5_z6>2)1z$)abA-s(fyjJDT_ z0bMW$Bv00)*9#DM?srJNk1}Guq_3iTGlMzB;q3vf?iM5uFjR;gAh%O&OZbg`x9xv^0d{Q@yom#X4NO|MyDN zL$a1){IOSa{R763)H7;Axh5H*8BpGRpx$2T=~mL_Wb(hz&F6s1^X9L}1uadU4cF02 zbkNOo1!$Zk*E~4OocUW+Vq9)ka|X9a;Ay)2hyo<+-{g7N zzp0kR${4!UUvpy9@nja_GirnQruTx0yW)Dg^an8QtZ;mQ>ihN65a&nq&Q9V;5_d0T zKivqHe2j%ON`)CD-iq{lEj& z^XXD|Yy`Z*$xwlpWB`@><)H=VTOkes_K8Q}r0mXKHA`f)%ni}K_>G(Tw^0YRet3Tc z>ZAUZ@f}19Iv+x-0QhcK)N5=mgw9eRwk2$0Kc%C#RiSIwpv0KC3VGB={4H#aZ$&{V z(tK)CJX5&+saKi4rf@efuU(Y9d$#rV{k1)Q`_)fR+bRxC8-!-7=X18UW%j7zeN9G5 zjuLoC-f={mzGnQlU!BpT9?ufBc(hvBe~udQs#TTCR(L=70<9267onn`;bQSC)p27# z%;6V*u^1sPgMN;({#3Id##GVDzrDlh$A?F%u9C#J^oe{<6-aKzgUMsN)ip&vFJr-AR#f%{<^CY@Y}01 z;4u062Ah^A(?s4GQWH*Bx^p_Y6jfmc6ig`k0lgO$<#X48d=06+Ok0+^%HFWHZ#peUAK<*Wx1fy+=adWe(5tNi2VBfm{C{qo<=`O?B}%K4jqBO zI{Fn~&l+R+N@ZrqPn_Q5M)4q@n*R$@9L$LQ{P-q zWdfNf*gvyLPw--8vHeNVyx|bfMzysw!fwh$dYeNUOa7C>ox-R@k~&BIkJ*5i;274L96) zD}8}nB6VRF0|U#$*WYZP$yV&U?&ra#5}w08l?O18~0 zF;38{kHTb6dM>6vlpWb__niO5<wPdpCFiF!RyOdx}aB^enV;l>+oAfM023Wj^b`LJTH;CT&}zrUE{L7Ol$ zZSVOieO&Opq!OBUU#ghV67BHN&zA#RD%Uc{@o<7(eKaOG`1@L^ha2;%z2TH0`Jg5d}fMjj%O9|G| z3we0M*}~$k=%>Im`8mK6oaR&&_vO}xYa$<^p`#gIeijJUHCX9p)c^_H?upXhQPqzN zzNB11vvrAZ{)*^dnuxG-*Hi4+y~hc9tr_!u2V2R!G>jfZXL})`_}MRhjqgPakO=t0 zbD=3|w|zF~wYB|8)k@UC_cbeK30JI*RhPlXi1{X5=D4cj1iiXwU^Q?a5e!8eK=$f$ zVC}*Nw#=c_eU%Q~ZPovSlt6n5uGwf3h(t+YskP?dp>7_$|9(=bloRz@p^Y0^>ykTJ zXM54gjJXv9On=tJEo(2oS;_^8`pg;W(B$I5kzZfZ-RGT3HU)e#)dS{LtX^|618!2 z+N3)g&1I)3=66IpUTaOEP~eWc?xv7vc7k35w9VP8jYLTyZG=2?6x(fdGQm%8zLSyk zXk>R)A_4Dz0jBy&BU7w?L@_X~HXmyySQjjZRkMgviOr6KK5#D3f)n(r;RL<9q<}w& zdXN&h_!L;PSnvwdLrgTrO<1$f(;CsRa;9+pN{O128i3zF@GxE`-LR9XMqupSEr|Be znI8P~LE~yCblnObzr5q#$e$qs>*mAdo50K4*BbYl3V{d1nh7<)PHJbj!wXKeMn?IE zV=m+Az)YOmy=pi?uP#f0(}JJN{;oA})h3W&nXr)u9|+upf(hiQp+%uFmf361jIrqF z`|R4in|*utkaTYM$`b^=iG3D9=LgXvPoeXD2!2p}NQ&0`(9kfy`~72R9j$nd2Ch66 zTAS_G&&KXg#o*FJj9F?-iiKM=ylQLUF^O)aFT{wtea%r}oS;_^!R)YNHumGL!D7{uLmo_;$_h3 zgXqzNXx~GS(0fXgvz`08=^Ge|B*+TrY=KMGgO`h44z#WiTSq1cjH_j|zURY+1;S-( zVvS8cCbzqzN;pBU9zL*^7(1|+&4xv@Ot4F%TN6-95GSg(1CWT|`OwxXT(l}$bgup0 zZ@c|Y%;@P9eKKerq(CXoj0Dj+$R5ETJphGa!a?WZ`(6j@>f4{(LoSz#)F(v2!Y){| z$mAxU7KWi+Mi*_Ybf6aZr=3`-gz(%`VttQ-Sx0vrs!BLPuMUE-%vWGSSIWx_hY2=W zI}hfx6k~*@1rkK%mUiOTXv+=8qB(zsL2avh7CN0RpUS_P*--iMWQVCjrVXqY5}duMPqoZxxu666+7BmzF(IflOnZw{` z#$JdCE{gqreXa3458v1H_YXxXYim!tc&5p;I4w!&nkL8*OkiPivoNPUa)0tMx5Nyb zgup{Sq%eCfBlos>9rkXYKKFd_FG*1I3|+p4=4R+>jxOlK!^7-)Y9G?vL9(d{ z-}j3tM_r!x5L%Zk?XYXTFoGW0i%uUv``Msk7=S$OV%YD~K0MDSkw}p0?4WOCl-+v| z#`?bJbw!<3Hvsml8?zO(cFS0)1k7lWxF0#QvMq90TuHm?;Zqt%BFRX;Sk`fx1n~e! z;C8o>?lsgw0el|{I$lh|H7~iA3okg2%P+l{v(G)7b*on~V@6jH+T$1Vzr*x#=|&=m zNVngu=mOeHqjLjLNSnJjl!RdXcVq-S(08Q}fzi<{yY_c;|9y|}t2^)I_Pg)rz@eU4 z&C7c=zvXIz)+T+eR$zAd)uLF7E-s?cH$Y zMzLL|>_h7wZLD)0BR!$a`|z4zELe)6*$`PmJ>VDX}btXs31OD?;Z z=U#p(TQ+T^rDF#8dAwX6-}j0&x~H$W2Nt+>uZtiEqVwqdkl9b}XM_9$PZyc6I~@(F zrWB--c$q9uJh_Ko-E|)~-Fi3MckboDp&qiiT;#py=5x&R6Nf0)^0s^4rwxJ`VLa>k zs_x&WgWLe=QO*8=*u`6WnSS%Dy1)r~^%yVcC&fl#-~0%?;X-IhKw>%#K488LI{;OA zvSKa2WHa3GxTZf7Zxk_N;-j_CfrE$GxBmdQ-F^pu|DlhtcJ(S={0A@Kr7wOFr)}9n zOIruZ%TdVZ(Y}vTsuaM~=+`nN;Kem^QJ@RxY(M1t(0&0#m0as3A3I=*d}x7GB7tg7 zkuP{WvHdB2a?_oB|Hrp+-$RcV-(G*d-|W^r5#3>;FfHw`Y|9*$X?S1tl=21HSkMxau@1s(izX z0m1BEY5YM2`J&8K>9dTQh=QF*eV+RTpF=S%^odYP#+<9EDaE?AYdPnSu>e8Z!RmAFXODU&g9&4p3SCHPhr9Q`KYEgz(>5ivE(NZcze#Ny5<^6E_Kr& zvqJ0VjELul(S8=~d%?uUD0niNL{bS*33M*cK>q-b?%2!!-v0={yKfuYckZL7w~yXq z17xzfP_6~Ld!X`vi(ry9-TE%G-Aa4r)dufQ#r~u-3V%dZ&E+2MPa5GJKlS+BZzK07 zyW;*s;Fb2&m2Ih`!EfohMq%pz{u*e*?%*i{JJ4Ld0e<$j(*D^-U;ir(K%)wOR8__0 zZpG2lkWj)0Zua=(9U3oYKwKO|#H^!hU5tnaXl-p_If+CSfpb`o%nZN_Q z(JV*0kMQW@ySeLsALKXpKFkB#b}%qFJnp-ZNx9OaV!xp!v*6zM!_4+z9ZWa2s2BTF zHW2hG_9u;5{`=~^9)JBaO{QSt63RVI==qJn*T=o?s1OZ&JJ&rnxy&8FnW5LpJ6Md6 zzu|oNhZh?+yh)kbxS~GBqDWVM01CSW^S*ze2)O^O~j zTbW3hC!lyA*)~b(+!`P8Gf+q)*+s~0hN7#4ZFnbRi=D@)t z?B08jJ>&AN+ewGD=B8?!pH15I+*9F`uYxu6LHjm&Q)Bigvut746GjG;mAMT z2TGZlq1|f~tl|5?@(@UZ8gp7!aBKvKb(%i#;JhmA`FFQ+lil*Y5xi(jn= z{*VfC;a93Z1wR9ME&R*v9{>7l&0r>$nA_*?Vl4C=(=ataJ|h`ttDC z|LyVJ2hC(T#@=&{5$`lc%zrSc20BhiW8Aroi}f`ACGh`%=FscqpCmZTf-ZRT#qhd| zgyl0q`^9}h4KWCf6?D6uD_*<9!lM#!WEj5xpyo4o`|Ruu-ZPoT))C=r8~%ABvA65) z;y%S2I)_B=Yi-z(0!O}f6|yVxr14_ z>2b}c@6z16Ur(k`loj-QfL8#AaMpR%C_D1n5gUcMi7x~e6VdG+W+Vr{*$KBk3e8E_ zxCC07go0NZgc>1mPBf5<9_1buA0Jb|CZU8IAJ@F+=RRM(SJRuGK0-bb;nb+*U~UDm z2bcZbJQLE|W(#oN9=K&2q;s%kDYUlyAFX#`ii06Ht78^62;+odYd_}U10Q%bmJ zpXP6G)cpIeHGBIf)t~Izc>;I`M%0~{SCKluRi_OKcn$C+%*|~Rphm_9_xKvlT@C;E zVtDpCXb#+DsyKkx;r?}NQH*gB!pXjh@W7)1h2SaJc0lv@w|Lz6#Mm&gcr7b7#N+9c zt~&Y>pV8=XKbza3MC9uvm@os+hf@~7hyMVs+6t}7@X}VD_9ucsQ#Afy0_{SG_zY9VyC%0r}kMx0WW47={-09|sN`2j((-*>{alRGvGO!h4u5G zDHZgmpjMcwm;OW`=vw0sC(s2sVO&aB$dxhys=dQ-&py+iZhAt~Jyhy*lk87|5%WiY z7XtexcS=V&)X$wc{ZW`IL;nR_NsIuw@z_qM1e#N@Y!;li244AWxbzfgZV_|=@&$lW zAl+2D=8FT+V4~=Jv(>jmOn{ZO3Q`Fnk%Vj>?%S{V?t_}!pY(aE-?&KiV?5$ap>QR^9#lbjH9xIFROLY+6mPFa5M1V!0&-~VS=roLf1kGBonZB2Ar`R zu00#B-UOW;0v_b@ripdapV0Wj3Y}BMJ``@3?N3Q1BvVlE;L*dHA3mh{#dgh;M>YMU zp>s~MNx`!R90UFo_*w9~6Z9(M2BX>{z^TA@FzF8`ZCMmg9CF12Y*+}dI165|8P+X< zwv>>-SQ#2U*4zvtUdF7qRtX(!pVC9dMQFQt(F_j$kaPj|^lEP1q4~i>n)?rGQ#DIM z=czz*D7o3Q!0RwVVMSyn@I2rTfwM4{dvmzC^n`#FM}x z%(l=ADa8a;rawt3dCKCM@bWX^n$uzH3TSH;Qh^ujY5l3#gzAg@ctJ<>7;lxrq(2_@ z;ZVQkj-8qxJgm9%DNQDCJ$Mz{4H9;8F zn%tT);(o?**t`TbEP+*Xg+-mv+5}n!YlA*T>$sSQJKQWOzk#~=Kl6k6T|l%MV8plx zDVXTXb zBQQ5~p0LI4`c-*M$2QEQYy5i!lepr+6C>+fHLb`wm%hf{uH}WsafS}y&m_24k@;Xm^1@vF%~+STtVp1Xm4lT`?h57JM>znm!+xd&GZe1FStwm#J{C16Q z%<$@I@;T^F!>)abM-IS4hcu5LfxZ34?KhZ((R`2#r7|w{i3nQ~{lGhcZv?-O|H$!6 zBPn1uFbB8@Gda5$W4+t=+4D%~as0*2>CgB{&v-XF7%Tif;6~ss%=&mQkO`e5{5+l& z!1pi{uJPxt2;=)xQb9)x%xr~)Ghp3()1Njkf%WrYZZK(^F}q`G)Ss-U>mPwn;S$KyoM-9) zGu0@}jea^tNH#Eb3t?5Ri%lnMt?)3dDOfNAR?LP~b7A#7vxYdQ1G-vaMk{nQLsQaB z43sJb+i5hu2YJt|&yD0@U=#+@&^rP>L$I?4b{&PiN8!MLSpyHd28B@Ds`1AYG<7Wf zz+VGjqjXJ9hMlH(q1R>t=L65dxPF&o^7pr5uIoes7Czr}Vxr3zwx%^kq|+GB$UVTV z7!f~!`L4o~pmJ^p``*>SS1{MWz8-$AC_*rC)lau{n-Vau6PC|`RdZm?Jd;a)PKTKQ z%xHtoP=87!2)tk>tWA5Gyl?tcCg@N7qtG`3J;U(SQP|Z3PxZq7J|pVm+mNVjfATTc z^`9|m8ZzAmlSaaR_h$otk6Eh?zh3ogV=ZbsbAQ?rx(%jDm>S4UNi#?!f{dc6 zK%NS|+4kcFR(#HbOdfKD(CMUf&XN^Ua?jV$C+7!`hk*BD#3X1%WI}&c@#hwd*JUGc z8fLP#3?uUM8M_7_K0f?=BlXMiqC5U?(wH^xeZj-J(jNn!#JJbPf*(FMDgvwlJ`B7Z zGnuM=f5NV9kNf1{K2IpqpIVZozLXGS@hfE}#hz~p4Ea{n^Jd>X$2ljIGW^!CKZURT zPE7KwFEiE~_}Y!!lH3K z@VA&jsBof#A`o_~hrg!w;L(K<_RSc#@K#_Y(81Hc7R}i9PP8sxEjRvn_xSPJ1SPLh z_`P}HAn+h?Z}7Ye;|&?cSms{nxZ{0ZTomRz{UhK5n57X-X=g%1_NVdZnBx8vUOG`2 zFU#97yAE75`xN2?y*{u5<7tdP`wh}#H5cnkKJ1*D7K3VMQXLxo7YWHU-ZsF)sWj%zC|jBq@o~cwrq+l>6m)F7ol~{{@U#7cgt1d0-T?)$d7+)%`fI z9g`U`KdwDt4XXBlpTJ!2D>1=xizt16wmpvMgs+>2iF^1yM$FxMc^%+x?E1l8iw(eA zF;&`Tj{7%vy~<;J0F{Ta%Krnno3Ub3OTyRDANQJN2Bo08IT+EO1uVe0?7J{)^KF!@ z|MuYb6h^cX0Gf1aHk3mebogPe(T7U4|8@xWi)4d$w2|+Ccd?M`k zx&(MLX0Q4}{(pPt_hMBQ#qrPFxg-Vy+T@HiArjG3^ddw?4;lDUpg}~hiTGdi+(WNH z_0V6?O9T-_g7pwSgn^U=Pj^6X*`+?0l_sl+*JNNFj z*IsL{waz_OmK!kncN_s<0`H?B#+#{+nIaz0?PG-YSr4KBlLKtrV=d2(2t|^W<&Vhq zdJ-w0=+`E)%emZc3Cn(#+EO1^ug683N&QOe9%>6aTD*?D;rzGNqF=blb5R{w|6RB3 zdj);oG1}sI8#qE8Yi{MfHCdiw+v0cN9h6bQX`zlW^LaKm<)ru4?_jkuR!vZgV$9q-)+vb+B%il2A}c{=jnEl;A9r#0k_ z`4o5;_=N|zWo7OH58(Eaf14%X72r)|x$h|Y-LC+twvzwXa#A^iCZ11FBz3;W44W7; z3&3IEb>I*%SM(XOJeBbo=a4*ojAW|?@vUYvhB9EgPPjSeQSSN);8nD53d3<9mXL*R4d;ab7+)Oz|lQ2R=_ zxuVkC31qp?qhKl6nBvs+-#EK{V?S?%g10UM-vA#`2fO7+aAUS2#{=vK-U8l07JLp! z-7F=si)`wEP5*B7u7XeJ&cH^~#gPbiQ4E8sgM zUpWiJ^3);PGyu*2Bdm-ub@7syP>u2DksC3u#>2Iib8@FK8I==GcIV}SC2Oc)#b@ZT zndeco`VAbIW451A+_Pw5yoi+hk?OdMJCmn4e^4|j@Hn3>u6>VW=_IO{_zy}^v^;ej zEA;*l6H!bsAlKmwz{_Z_@NuN{BX1d+)1~rt*l3O1j#~}qrDafFpCC8-RrDO?GpN4z zX{5w+>I`_|G541;kA4O{UGD&T7|(O4obMd9JY|KrRo1|E%2Qm+ed4jRSF83n@B@;k zQ>grIRCLSphUqv~=>4S04Y(6o8~dvE6td%{j&8SZNwN_XLkLsmoiIqd#3)1Z{68I(f$81O{ZoF~)(&jm;xLaA_jP&KUuG$BRXi8UbxF`PpJHZKjl`5w$FZJUv>qy=d{g zg!bciSI1q{75?JB5t5+^+E$8o4aw0Zq_CH(^JTO!4tZKhEqfU(PXoXT-56tx=>b;g z#u#Hv53oWv#u#IIfEBti#u(EBtk8`y#+V*pg>H;7#`FLybYqM$rUzJ|8)J+yJ-`ax s7-Nj-0aob77-LKiutGP+7-M?Ce`H7}|H~@!umAu607*qoM6N<$f`3|_EdT%j literal 0 HcmV?d00001 From 952eb3d8d75d79abea4bd1f946d696fb4d5316de Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Tue, 29 Jul 2025 21:19:38 +0700 Subject: [PATCH 03/12] Update script URL to deploy from CDN --- application/src/components/servers/ManualInstallTab.tsx | 2 +- application/src/components/servers/OneClickInstallTab.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/components/servers/ManualInstallTab.tsx b/application/src/components/servers/ManualInstallTab.tsx index 9d289a8..757e5a0 100644 --- a/application/src/components/servers/ManualInstallTab.tsx +++ b/application/src/components/servers/ManualInstallTab.tsx @@ -25,7 +25,7 @@ export const ManualInstallTab: React.FC = ({ onDialogClose, }) => { const getManualInstallSteps = () => { - const scriptUrl = "https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/server-agent.sh"; + const scriptUrl = "https://cdn.checkcle.io/scripts/server-agent.sh"; return [ { diff --git a/application/src/components/servers/OneClickInstallTab.tsx b/application/src/components/servers/OneClickInstallTab.tsx index 1067d20..24e5f6f 100644 --- a/application/src/components/servers/OneClickInstallTab.tsx +++ b/application/src/components/servers/OneClickInstallTab.tsx @@ -27,7 +27,7 @@ export const OneClickInstallTab: React.FC = ({ onDialogClose, }) => { const getOneClickInstallCommand = () => { - const scriptUrl = "https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/server-agent.sh"; + const scriptUrl = "https://cdn.checkcle.io/scripts/server-agent.sh"; return `curl -L -o server-agent.sh "${scriptUrl}" chmod +x server-agent.sh From bc06a24adbaf42b9194140631280d4955e834739 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Thu, 31 Jul 2025 02:44:51 +0700 Subject: [PATCH 04/12] feat: Add Docker One-Click Install Tab - Adds a new "Docker One-Click" tab to the server dashboard and create new server dialog. This tab provides a one-click installation script for the Docker monitoring agent, dynamically populating it with server-specific details. --- .../servers/AddServerAgentDialog.tsx | 14 +- .../components/servers/DockerOneClickTab.tsx | 176 ++++++++++++++++++ 2 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 application/src/components/servers/DockerOneClickTab.tsx diff --git a/application/src/components/servers/AddServerAgentDialog.tsx b/application/src/components/servers/AddServerAgentDialog.tsx index 755bb3d..65c1645 100644 --- a/application/src/components/servers/AddServerAgentDialog.tsx +++ b/application/src/components/servers/AddServerAgentDialog.tsx @@ -14,6 +14,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { getCurrentEndpoint } from "@/lib/pocketbase"; import { ServerAgentConfigForm } from "./ServerAgentConfigForm"; import { OneClickInstallTab } from "./OneClickInstallTab"; +import { DockerOneClickTab } from "./DockerOneClickTab"; import { ManualInstallTab } from "./ManualInstallTab"; interface AddServerAgentDialogProps { @@ -123,9 +124,10 @@ export const AddServerAgentDialog: React.FC = ({ - + Configure Agent One-Click Install + Docker One-Click Manual Installation @@ -150,6 +152,16 @@ export const AddServerAgentDialog: React.FC = ({ onDialogClose={handleDialogClose} /> + + + + void; +} + +export const DockerOneClickTab: React.FC = ({ + serverToken, + currentPocketBaseUrl, + formData, + serverId, + onDialogClose, +}) => { + const getDockerOneClickCommand = () => { + const scriptUrl = "https://cdn.checkcle.io/scripts/server-docker-agent.sh"; + + return `curl -L -o server-docker-agent.sh "${scriptUrl}" +chmod +x server-docker-agent.sh +SERVER_TOKEN="${serverToken}" \\ +POCKETBASE_URL="${currentPocketBaseUrl}" \\ +SERVER_NAME="${formData.serverName}" \\ +AGENT_ID="${serverId}" \\ +sudo -E bash ./server-docker-agent.sh`; + }; + + const getDirectDockerCommand = () => { + return `docker run -d \\ + --name monitoring-agent \\ + --restart unless-stopped \\ + -p 8081:8081 \\ + --group-add 999 \\ + -e AGENT_ID="${serverId}" \\ + -e SERVER_NAME="${formData.serverName}" \\ + -e SERVER_TOKEN="${serverToken}" \\ + -e POCKETBASE_URL="${currentPocketBaseUrl}" \\ + -e POCKETBASE_ENABLED=true \\ + -v /proc:/host/proc:ro \\ + -v /etc:/host/etc:ro \\ + -v /sys:/host/sys:ro \\ + -v /:/host/root:ro \\ + -v /var/run:/host/var/run:ro \\ + -v /dev:/host/dev:ro \\ + -v /var/run/docker.sock:/var/run/docker.sock:ro \\ + -v monitoring_data:/var/lib/monitoring-agent \\ + -v monitoring_logs:/var/log/monitoring-agent \\ + operacle/checkcle-server-agent:latest`; + }; + + const handleCopyOneClickCommand = async (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + console.log('Copy one-click command button clicked'); + const command = getDockerOneClickCommand(); + console.log('Copying one-click command:', command); + await copyToClipboard(command); + }; + + const handleCopyDockerCommand = async (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + console.log('Copy docker command button clicked'); + const command = getDirectDockerCommand(); + console.log('Copying docker command:', command); + await copyToClipboard(command); + }; + + return ( +
+ {/* One-Click Docker Installation */} + + + + + Docker One-Click Install (Recommended) + + + Automated Docker container installation with system monitoring capabilities + + + +
+ +
+
+                {getDockerOneClickCommand()}
+              
+ +
+
+ +
+

This script will automatically:

+
    +
  1. Download and setup the Docker monitoring agent
  2. +
  3. Configure all required environment variables
  4. +
  5. Start the container with proper system access
  6. +
  7. Setup monitoring data persistence
  8. +
+
+
+
+ + {/* Direct Docker Run Command */} + + + + + Direct Docker Run Command + + + If you prefer to run the Docker container directly without the script + + + +
+ +
+
+                {getDirectDockerCommand()}
+              
+ +
+
+ +
+

Prerequisites for direct Docker run:

+
    +
  1. Docker must be installed and running
  2. +
  3. The operacle/checkcle-server-agent image must be available
  4. +
  5. Run as root or with sudo privileges
  6. +
+
+
+
+ +
+ +
+
+ ); +}; \ No newline at end of file From 44bc2bc1e56223798b960ee6f6d4da4f973d9e6d Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Thu, 31 Jul 2025 02:46:27 +0700 Subject: [PATCH 05/12] feat: Create server-docker-agent.sh script - This script will automate the process of running the monitoring agent Docker container. It will take environment variables as input and construct the `docker run` command with the provided values, including volumes and ports, to ensure a seamless one-click installation experience. --- scripts/server-docker-agent.sh | 361 +++++++++++++++++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100644 scripts/server-docker-agent.sh diff --git a/scripts/server-docker-agent.sh b/scripts/server-docker-agent.sh new file mode 100644 index 0000000..49948c3 --- /dev/null +++ b/scripts/server-docker-agent.sh @@ -0,0 +1,361 @@ + +#!/bin/bash + +# CheckCle Server Monitoring Agent - Docker One-Click Installation Script +# This script provides fully automated Docker installation using environment variables + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Helper functions +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Check if running as root +check_root() { + if [[ $EUID -ne 0 ]]; then + log_error "This script must be run as root (use sudo)" + exit 1 + fi +} + +# Check if Docker is installed +check_docker() { + if ! command -v docker >/dev/null 2>&1; then + log_error "Docker is not installed" + log_info "Please install Docker first:" + log_info " Ubuntu/Debian: sudo apt-get update && sudo apt-get install docker.io" + log_info " CentOS/RHEL: sudo yum install docker" + log_info " Or visit: https://docs.docker.com/engine/install/" + exit 1 + fi + + # Check if Docker daemon is running + if ! docker info >/dev/null 2>&1; then + log_error "Docker daemon is not running" + log_info "Start Docker with: sudo systemctl start docker" + exit 1 + fi + + log_success "Docker is installed and running" +} + +# Validate required environment variables +validate_environment() { + local missing_vars=() + + # Check required variables + [[ -z "$SERVER_TOKEN" ]] && missing_vars+=("SERVER_TOKEN") + [[ -z "$POCKETBASE_URL" ]] && missing_vars+=("POCKETBASE_URL") + [[ -z "$SERVER_NAME" ]] && missing_vars+=("SERVER_NAME") + [[ -z "$AGENT_ID" ]] && missing_vars+=("AGENT_ID") + + if [[ ${#missing_vars[@]} -ne 0 ]]; then + log_error "Missing required environment variables: ${missing_vars[*]}" + log_info "" + log_info "Usage examples:" + log_info " SERVER_TOKEN=your-token POCKETBASE_URL=http://host:8090 SERVER_NAME=server AGENT_ID=agent sudo -E bash $0" + log_info " sudo SERVER_TOKEN=your-token POCKETBASE_URL=http://host:8090 SERVER_NAME=server AGENT_ID=agent bash $0" + log_info "" + log_info "Required variables:" + log_info " SERVER_TOKEN Server authentication token" + log_info " POCKETBASE_URL PocketBase URL (e.g., http://194.233.80.126:8090)" + log_info " SERVER_NAME Server name (e.g., DB-Server01)" + log_info " AGENT_ID Agent identifier (e.g., agent_hogr3np88u7dq5xf9balzl)" + log_info "" + log_info "Optional variables:" + log_info " HEALTH_CHECK_PORT Health check port (default: 8081)" + log_info " CONTAINER_NAME Container name (default: monitoring-agent)" + log_info " DOCKER_IMAGE Docker image (default: operacle/checkcle-server-agent:latest)" + exit 1 + fi + + log_success "Environment validation passed" + log_info "SERVER_TOKEN: ${SERVER_TOKEN:0:8}..." + log_info "POCKETBASE_URL: $POCKETBASE_URL" + log_info "SERVER_NAME: $SERVER_NAME" + log_info "AGENT_ID: $AGENT_ID" +} + +# Set default values for optional variables +set_defaults() { + HEALTH_CHECK_PORT="${HEALTH_CHECK_PORT:-8081}" + CONTAINER_NAME="${CONTAINER_NAME:-monitoring-agent}" + DOCKER_IMAGE="${DOCKER_IMAGE:-operacle/checkcle-server-agent:latest}" + POCKETBASE_ENABLED="${POCKETBASE_ENABLED:-true}" + + log_info "Configuration:" + log_info " Container name: $CONTAINER_NAME" + log_info " Docker image: $DOCKER_IMAGE" + log_info " Health check port: $HEALTH_CHECK_PORT" + log_info " PocketBase enabled: $POCKETBASE_ENABLED" +} + +# Stop and remove existing container if it exists +cleanup_existing_container() { + if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + log_info "Stopping and removing existing container: $CONTAINER_NAME" + docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true + docker rm "$CONTAINER_NAME" >/dev/null 2>&1 || true + log_success "Existing container removed" + fi +} + +# Create Docker volumes +create_volumes() { + log_info "Creating Docker volumes..." + + # Create volumes if they don't exist + if ! docker volume ls --format '{{.Name}}' | grep -q "^monitoring_data$"; then + docker volume create monitoring_data + log_success "Created monitoring_data volume" + else + log_info "monitoring_data volume already exists" + fi + + if ! docker volume ls --format '{{.Name}}' | grep -q "^monitoring_logs$"; then + docker volume create monitoring_logs + log_success "Created monitoring_logs volume" + else + log_info "monitoring_logs volume already exists" + fi +} + +# Get Docker group ID +get_docker_group_id() { + local docker_gid + docker_gid=$(getent group docker | cut -d: -f3) + if [[ -z "$docker_gid" ]]; then + log_warning "Docker group not found, using default GID 999" + docker_gid=999 + fi + echo "$docker_gid" +} + +# Run the Docker container +run_docker_container() { + log_info "Starting monitoring agent Docker container..." + + local docker_gid + docker_gid=$(get_docker_group_id) + + log_info "Docker group ID: $docker_gid" + + # Run the Docker container with all the required parameters + docker run -d \ + --name "$CONTAINER_NAME" \ + --restart unless-stopped \ + -p "$HEALTH_CHECK_PORT:8081" \ + --group-add "$docker_gid" \ + -e AGENT_ID="$AGENT_ID" \ + -e SERVER_NAME="$SERVER_NAME" \ + -e SERVER_TOKEN="$SERVER_TOKEN" \ + -e POCKETBASE_URL="$POCKETBASE_URL" \ + -e POCKETBASE_ENABLED="$POCKETBASE_ENABLED" \ + -v /proc:/host/proc:ro \ + -v /etc:/host/etc:ro \ + -v /sys:/host/sys:ro \ + -v /:/host/root:ro \ + -v /var/run:/host/var/run:ro \ + -v /dev:/host/dev:ro \ + -v /var/run/docker.sock:/var/run/docker.sock:ro \ + -v monitoring_data:/var/lib/monitoring-agent \ + -v monitoring_logs:/var/log/monitoring-agent \ + "$DOCKER_IMAGE" + + log_success "Docker container started successfully" +} + +# Wait for container to be ready +wait_for_container() { + log_info "Waiting for container to be ready..." + + local max_attempts=30 + local attempt=0 + + while [[ $attempt -lt $max_attempts ]]; do + if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + log_success "Container is running" + return 0 + fi + + sleep 1 + ((attempt++)) + done + + log_error "Container failed to start within expected time" + return 1 +} + +# Test the installation +test_installation() { + log_info "Testing installation..." + + # Check container status + if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + log_error "Container is not running" + log_info "Check container logs: docker logs $CONTAINER_NAME" + return 1 + fi + + # Wait a moment for the service to start + sleep 3 + + # Test health endpoint + log_info "Testing health endpoint at http://localhost:$HEALTH_CHECK_PORT/health" + + if curl -s -f "http://localhost:$HEALTH_CHECK_PORT/health" >/dev/null 2>&1; then + log_success "Health endpoint is responding" + else + log_warning "Health endpoint not responding yet (service may still be starting)" + fi + + # Show recent container logs + log_info "Recent container logs:" + docker logs --tail 10 "$CONTAINER_NAME" +} + +# Show post-installation information +show_post_install_info() { + echo + echo "=============================================" + echo " Docker Installation Complete!" + echo "=============================================" + echo + log_success "CheckCle Monitoring Agent Docker container installed and running successfully" + echo + echo "Container Information:" + echo " Container name: $CONTAINER_NAME" + echo " Docker image: $DOCKER_IMAGE" + echo " Health check port: $HEALTH_CHECK_PORT" + echo " Agent ID: $AGENT_ID" + echo " Server name: $SERVER_NAME" + echo + echo "Useful commands:" + echo " Container status: docker ps -f name=$CONTAINER_NAME" + echo " Container logs: docker logs -f $CONTAINER_NAME" + echo " Stop container: docker stop $CONTAINER_NAME" + echo " Start container: docker start $CONTAINER_NAME" + echo " Remove container: docker rm -f $CONTAINER_NAME" + echo " Health check: curl http://localhost:$HEALTH_CHECK_PORT/health" + echo + echo "The monitoring agent is now running in a Docker container and will appear in your dashboard." + echo +} + +# Main installation function +main() { + echo "=============================================" + echo " CheckCle Server Monitoring Agent" + echo " Docker One-Click Installation" + echo "=============================================" + echo + + check_root + check_docker + validate_environment + set_defaults + + log_info "Starting Docker container installation..." + + # Setup and run container + cleanup_existing_container + create_volumes + run_docker_container + + if wait_for_container; then + test_installation + show_post_install_info + else + log_error "Container installation failed" + log_info "Check container logs: docker logs $CONTAINER_NAME" + exit 1 + fi +} + +# Handle script arguments +case "${1:-}" in + --help|-h) + echo "CheckCle Server Monitoring Agent - Docker One-Click Installer" + echo + echo "Usage: SERVER_TOKEN=token POCKETBASE_URL=url SERVER_NAME=name AGENT_ID=id sudo bash $0" + echo + echo "System Requirements:" + echo " - Docker installed and running" + echo " - Root privileges (sudo)" + echo " - Internet access to pull Docker image" + echo + echo "Required Environment Variables:" + echo " SERVER_TOKEN Server authentication token" + echo " POCKETBASE_URL PocketBase URL (e.g., http://194.233.80.126:8090)" + echo " SERVER_NAME Server name (e.g., DB-Server01)" + echo " AGENT_ID Agent identifier" + echo + echo "Optional Environment Variables:" + echo " HEALTH_CHECK_PORT Health check port (default: 8081)" + echo " CONTAINER_NAME Container name (default: monitoring-agent)" + echo " DOCKER_IMAGE Docker image (default: operacle/checkcle-server-agent:latest)" + echo " POCKETBASE_ENABLED Enable PocketBase (default: true)" + echo + echo "Examples:" + echo " SERVER_TOKEN=abc123 POCKETBASE_URL=http://host:8090 SERVER_NAME=server AGENT_ID=agent sudo -E bash $0" + echo " sudo SERVER_TOKEN=abc123 POCKETBASE_URL=http://host:8090 SERVER_NAME=server AGENT_ID=agent bash $0" + echo + echo "Options:" + echo " --help, -h Show this help message" + echo " --uninstall Remove the monitoring agent container" + echo + exit 0 + ;; + --uninstall) + check_root + check_docker + set_defaults + + log_info "Uninstalling monitoring agent Docker container..." + + # Stop and remove container + if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true + docker rm "$CONTAINER_NAME" >/dev/null 2>&1 || true + log_success "Container removed" + else + log_info "Container not found" + fi + + # Optionally remove volumes (ask user) + echo -n "Remove data volumes? (y/N): " + read -r response + if [[ "$response" =~ ^[Yy]$ ]]; then + docker volume rm monitoring_data monitoring_logs >/dev/null 2>&1 || true + log_success "Data volumes removed" + else + log_info "Data volumes preserved" + fi + + log_success "Monitoring agent Docker container uninstalled" + exit 0 + ;; + *) + main "$@" + ;; +esac \ No newline at end of file From 1fac88dcc9e97e241ec55d42d773b53728845b15 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Thu, 31 Jul 2025 02:52:57 +0700 Subject: [PATCH 06/12] feat: add docker compose for server docker agent --- docker/server-docker-agent.yml | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 docker/server-docker-agent.yml diff --git a/docker/server-docker-agent.yml b/docker/server-docker-agent.yml new file mode 100644 index 0000000..76823b2 --- /dev/null +++ b/docker/server-docker-agent.yml @@ -0,0 +1,35 @@ +name: checkcle-server-agent + +services: + checkcle-server-agent: + container_name: monitoring-agent + restart: unless-stopped + ports: + - "8081:8081" + group_add: + - 999 + environment: + AGENT_ID: agent_124343535 + SERVER_NAME: "test" + SERVER_TOKEN: srv_123243434 + POCKETBASE_URL: http://x.x.x.x:8090 + POCKETBASE_ENABLED: "true" + volumes: + - /proc:/host/proc:ro + - /etc:/host/etc:ro + - /sys:/host/sys:ro + - /:/host/root:ro + - /var/run:/host/var/run:ro + - /dev:/host/dev:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + - monitoring_data:/var/lib/monitoring-agent + - monitoring_logs:/var/log/monitoring-agent + image: operacle/checkcle-server-agent:latest + +volumes: + monitoring_data: + external: true + name: monitoring_data + monitoring_logs: + external: true + name: monitoring_logs From 205bedfba58618263e9a961c6698bc80d4962fc0 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Thu, 31 Jul 2025 02:58:02 +0700 Subject: [PATCH 07/12] Updated project development status --- docs/DEVELOPMENT_ROADMAP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/DEVELOPMENT_ROADMAP.md b/docs/DEVELOPMENT_ROADMAP.md index 5afac95..a767df0 100644 --- a/docs/DEVELOPMENT_ROADMAP.md +++ b/docs/DEVELOPMENT_ROADMAP.md @@ -68,7 +68,7 @@ The roadmap is divided into the following stages: #### Tentative Features: - [ ] ✅ Server and Service Table row clickable to detail page. - [ ] ✅ Implement pagination for the SSL dashboard table -- [ ] 🔧 Server Agent (RPM, Docker container, and general binary package) +- [ ] ✅ Server Agent (RPM, Docker container, and general binary package) - [ ] 🔧 Notification System (Webhook, Telegram, Discord, Slack, Email, Google Chat) - [ ] 🔧 Improve Uptime Service and Server connection update based on status and notification. - [ ] 🔧 Improve SSL perform the initial check automatically after creation From 8bb78e68eaf2dcc965ebf18b5a67cb161b1682d2 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Thu, 31 Jul 2025 14:57:16 +0700 Subject: [PATCH 08/12] Disable verbose logging - Disable log console output in file to prevent logs from being printed to the console. --- .../regional-monitoring/AddRegionalAgentDialog.tsx | 2 +- .../src/components/servers/DockerOneClickTab.tsx | 8 ++++---- .../ServiceDetailContainer/hooks/useServiceData.tsx | 2 +- .../src/services/ssl/sslCertificateOperations.ts | 10 +++++----- application/src/utils/copyUtils.ts | 10 +++++----- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/application/src/components/regional-monitoring/AddRegionalAgentDialog.tsx b/application/src/components/regional-monitoring/AddRegionalAgentDialog.tsx index c18cd18..ebde414 100644 --- a/application/src/components/regional-monitoring/AddRegionalAgentDialog.tsx +++ b/application/src/components/regional-monitoring/AddRegionalAgentDialog.tsx @@ -114,7 +114,7 @@ export const AddRegionalAgentDialog: React.FC = ({ textarea.setSelectionRange(0, 99999); // For mobile devices } } catch (selectError) { - console.error('Failed to select text:', selectError); + // console.error('Failed to select text:', selectError); } } }; diff --git a/application/src/components/servers/DockerOneClickTab.tsx b/application/src/components/servers/DockerOneClickTab.tsx index 9c23228..33719a9 100644 --- a/application/src/components/servers/DockerOneClickTab.tsx +++ b/application/src/components/servers/DockerOneClickTab.tsx @@ -64,18 +64,18 @@ sudo -E bash ./server-docker-agent.sh`; const handleCopyOneClickCommand = async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); - console.log('Copy one-click command button clicked'); + // console.log('Copy one-click command button clicked'); const command = getDockerOneClickCommand(); - console.log('Copying one-click command:', command); + // console.log('Copying one-click command:', command); await copyToClipboard(command); }; const handleCopyDockerCommand = async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); - console.log('Copy docker command button clicked'); + // console.log('Copy docker command button clicked'); const command = getDirectDockerCommand(); - console.log('Copying docker command:', command); + // console.log('Copying docker command:', command); await copyToClipboard(command); }; diff --git a/application/src/components/services/ServiceDetailContainer/hooks/useServiceData.tsx b/application/src/components/services/ServiceDetailContainer/hooks/useServiceData.tsx index fa42951..0fbcb6b 100644 --- a/application/src/components/services/ServiceDetailContainer/hooks/useServiceData.tsx +++ b/application/src/components/services/ServiceDetailContainer/hooks/useServiceData.tsx @@ -125,7 +125,7 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e } else { // Fetch regional agent specific data const [regionName, agentId] = currentAgent.split("|"); - console.log(`Fetching regional agent data for region: ${regionName}, agent: ${agentId} from ${service.type} collection`); + // console.log(`Fetching regional agent data for region: ${regionName}, agent: ${agentId} from ${service.type} collection`); history = await uptimeService.getUptimeHistoryByRegionalAgent(serviceId, limit, start, end, service.type, regionName, agentId); // console.log(`Retrieved ${history.length} regional monitoring records`); } diff --git a/application/src/services/ssl/sslCertificateOperations.ts b/application/src/services/ssl/sslCertificateOperations.ts index 88dc49f..d1e87a3 100644 --- a/application/src/services/ssl/sslCertificateOperations.ts +++ b/application/src/services/ssl/sslCertificateOperations.ts @@ -38,7 +38,7 @@ export const addSSLCertificate = async ( return record as unknown as SSLCertificate; } catch (error) { - console.error("Error adding SSL certificate:", error); + // console.error("Error adding SSL certificate:", error); throw error; } }; @@ -67,7 +67,7 @@ export const checkAndUpdateCertificate = async ( // Return the current certificate data return typedCertificate; } catch (error) { - console.error("Error updating SSL certificate:", error); + // console.error("Error updating SSL certificate:", error); throw error; } }; @@ -84,10 +84,10 @@ export const triggerImmediateCheck = async (certificateId: string): Promise => { await pb.collection("ssl_certificates").delete(id); return true; } catch (error) { - console.error("Error deleting SSL certificate:", error); + // console.error("Error deleting SSL certificate:", error); throw error; } }; diff --git a/application/src/utils/copyUtils.ts b/application/src/utils/copyUtils.ts index 24483d4..21f29ee 100644 --- a/application/src/utils/copyUtils.ts +++ b/application/src/utils/copyUtils.ts @@ -2,12 +2,12 @@ import { toast } from "@/hooks/use-toast"; export const copyToClipboard = async (text: string) => { - console.log('copyToClipboard called with text:', text); // Debug log + // console.log('copyToClipboard called with text:', text); // Debug log try { // Try modern clipboard API first if (navigator.clipboard && window.isSecureContext) { - console.log('Using modern clipboard API'); // Debug log + // console.log('Using modern clipboard API'); // Debug log await navigator.clipboard.writeText(text); toast({ title: "Copied!", @@ -16,7 +16,7 @@ export const copyToClipboard = async (text: string) => { return; } - console.log('Using fallback clipboard method'); // Debug log + // console.log('Using fallback clipboard method'); // Debug log // Fallback for older browsers or non-secure contexts const textArea = document.createElement("textarea"); @@ -39,7 +39,7 @@ export const copyToClipboard = async (text: string) => { document.body.removeChild(textArea); if (successful) { - console.log('Copy successful with execCommand'); // Debug log + // console.log('Copy successful with execCommand'); // Debug log toast({ title: "Copied!", description: "Content copied to clipboard successfully.", @@ -48,7 +48,7 @@ export const copyToClipboard = async (text: string) => { throw new Error('Copy command failed'); } } catch (error) { - console.error('Failed to copy to clipboard:', error); + // console.error('Failed to copy to clipboard:', error); // Show error toast toast({ From 50b4d1dfb0935ff8ce3a6fff15b53c93597179a4 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Thu, 31 Jul 2025 16:19:16 +0700 Subject: [PATCH 09/12] Refactor: Reduce verbose logging output in Regional Script - Reduce verbose logging in install-regional-agent.sh to provide a more minimal output while retaining all core functionality. --- scripts/install-regional-agent.sh | 229 +++++++++++------------------- 1 file changed, 81 insertions(+), 148 deletions(-) diff --git a/scripts/install-regional-agent.sh b/scripts/install-regional-agent.sh index dbf789d..92dba46 100644 --- a/scripts/install-regional-agent.sh +++ b/scripts/install-regional-agent.sh @@ -17,6 +17,30 @@ BASE_PACKAGE_URL="https://github.com/operacle/Distributed-Regional-Monitoring/re PACKAGE_VERSION="1.0.0" SERVICE_NAME="regional-check-agent" +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Helper functions +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + # Function to show usage show_usage() { echo "Usage: $0 [OPTIONS]" @@ -77,38 +101,36 @@ done # Validate required parameters if [[ -z "$REGION_NAME" || -z "$AGENT_ID" || -z "$AGENT_IP_ADDRESS" || -z "$AGENT_TOKEN" || -z "$POCKETBASE_URL" ]]; then - echo "❌ Error: Missing required parameters" + log_error "Missing required parameters" echo "" show_usage exit 1 fi -echo "🚀 CheckCle Regional Monitoring Agent - Universal Installation" -echo "==============================================================" -echo "" +echo "=============================================" +echo " CheckCle Regional Monitoring Agent" +echo " Universal Installation" +echo "=============================================" # Check if running as root if [[ $EUID -ne 0 ]]; then - echo "❌ This script must be run as root (use sudo)" + log_error "This script must be run as root (use sudo)" echo " Usage: sudo bash $0 [options]" exit 1 fi # Detect operating system -echo "🔍 Detecting system information..." OS_TYPE=$(uname -s | tr '[:upper:]' '[:lower:]') -echo " Operating System: $OS_TYPE" # Check if it's a supported OS if [[ "$OS_TYPE" != "linux" ]]; then - echo "❌ Unsupported operating system: $OS_TYPE" - echo " This installer only supports Linux systems" + log_error "Unsupported operating system: $OS_TYPE" + log_info "This installer only supports Linux systems" exit 1 fi # Detect architecture ARCH=$(uname -m) -echo " Hardware Architecture: $ARCH" # Map architecture to package architecture case $ARCH in @@ -120,33 +142,22 @@ case $ARCH in ;; armv7l|armv6l) PKG_ARCH="arm64" - echo "⚠️ ARM 32-bit detected, using ARM64 package (may require compatibility layer)" + log_warning "ARM 32-bit detected, using ARM64 package (may require compatibility layer)" ;; *) - echo "❌ Unsupported architecture: $ARCH" - echo " Supported architectures: x86_64 (amd64), aarch64 (arm64)" - echo " Please contact support for your architecture: $ARCH" + log_error "Unsupported architecture: $ARCH" + log_info "Supported architectures: x86_64 (amd64), aarch64 (arm64)" exit 1 ;; esac -echo " Package Architecture: $PKG_ARCH" +log_success "System: $OS_TYPE ($ARCH -> $PKG_ARCH)" # Construct package URLs and names PACKAGE_URL="$BASE_PACKAGE_URL/distributed-regional-check-agent_${PACKAGE_VERSION}_${PKG_ARCH}.deb" PACKAGE_NAME="distributed-regional-check-agent_${PACKAGE_VERSION}_${PKG_ARCH}.deb" -echo "" -echo "📋 Installation Configuration:" -echo " Region Name: $REGION_NAME" -echo " Agent ID: $AGENT_ID" -echo " Agent IP: $AGENT_IP_ADDRESS" -echo " Package Architecture: $PKG_ARCH" -echo " Package URL: $PACKAGE_URL" -echo "" - # Check for required tools -echo "🔧 Checking system requirements..." MISSING_TOOLS=() if ! command -v wget >/dev/null 2>&1 && ! command -v curl >/dev/null 2>&1; then @@ -162,129 +173,85 @@ if ! command -v systemctl >/dev/null 2>&1; then fi if [ ${#MISSING_TOOLS[@]} -ne 0 ]; then - echo "❌ Missing required tools: ${MISSING_TOOLS[*]}" - echo " Please install missing tools and try again" - echo " On Debian/Ubuntu: sudo apt-get update && sudo apt-get install wget curl" + log_error "Missing required tools: ${MISSING_TOOLS[*]}" + log_info "On Debian/Ubuntu: sudo apt-get update && sudo apt-get install wget curl" exit 1 fi -echo "✅ System requirements satisfied" - # Create temporary directory TEMP_DIR=$(mktemp -d) -echo "📁 Created temporary directory: $TEMP_DIR" # Download the .deb package -echo "" -echo "📥 Downloading Regional Monitoring Agent package for $PKG_ARCH..." +log_info "Downloading package for $PKG_ARCH..." cd "$TEMP_DIR" -# Test if package exists first - Accept both 200 and 302 (redirect) as success -echo "🔍 Checking package availability..." +# Test if package exists first if command -v curl >/dev/null 2>&1; then HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -I "$PACKAGE_URL") if [ "$HTTP_STATUS" != "200" ] && [ "$HTTP_STATUS" != "302" ]; then - echo "❌ Package not found at $PACKAGE_URL (HTTP $HTTP_STATUS)" - echo " Available packages should be:" - echo " - distributed-regional-check-agent_${PACKAGE_VERSION}_amd64.deb" - echo " - distributed-regional-check-agent_${PACKAGE_VERSION}_arm64.deb" - echo "" - echo " Please check the GitHub releases page:" - echo " https://github.com/operacle/Distributed-Regional-Monitoring/releases" + log_error "Package not found at $PACKAGE_URL (HTTP $HTTP_STATUS)" + log_info "Check: https://github.com/operacle/Distributed-Regional-Monitoring/releases" rm -rf "$TEMP_DIR" exit 1 fi - echo "✅ Package found (HTTP $HTTP_STATUS), proceeding with download..." fi # Try wget first, then curl as fallback DOWNLOAD_SUCCESS=false if command -v wget >/dev/null 2>&1; then - echo "📥 Downloading with wget..." if wget -q --show-progress --timeout=30 --tries=3 "$PACKAGE_URL" -O "$PACKAGE_NAME"; then DOWNLOAD_SUCCESS=true - echo "✅ Package downloaded successfully using wget" fi fi if [ "$DOWNLOAD_SUCCESS" = false ] && command -v curl >/dev/null 2>&1; then - echo "📥 Downloading with curl..." if curl -L --connect-timeout 30 --max-time 300 --retry 3 --retry-delay 2 -o "$PACKAGE_NAME" "$PACKAGE_URL" --progress-bar; then DOWNLOAD_SUCCESS=true - echo "✅ Package downloaded successfully using curl" fi fi if [ "$DOWNLOAD_SUCCESS" = false ]; then - echo "❌ Failed to download package from $PACKAGE_URL" - echo " Please check:" - echo " - Internet connection" - echo " - Package availability for $PKG_ARCH architecture" - echo " - GitHub repository access: https://github.com/operacle/Distributed-Regional-Monitoring/releases" - echo " - Firewall/proxy settings" - echo "" - echo " Available packages should be:" - echo " - distributed-regional-check-agent_${PACKAGE_VERSION}_amd64.deb" - echo " - distributed-regional-check-agent_${PACKAGE_VERSION}_arm64.deb" + log_error "Failed to download package from $PACKAGE_URL" + log_info "Check internet connection and package availability" rm -rf "$TEMP_DIR" exit 1 fi # Verify download was successful if [ ! -f "$PACKAGE_NAME" ] || [ ! -s "$PACKAGE_NAME" ]; then - echo "❌ Downloaded package is empty or missing" - echo " File size: $(ls -lh "$PACKAGE_NAME" 2>/dev/null | awk '{print $5}' || echo 'file not found')" + log_error "Downloaded package is empty or missing" rm -rf "$TEMP_DIR" exit 1 fi # Verify package integrity -echo "" -echo "🔍 Verifying package..." if dpkg-deb --info "$PACKAGE_NAME" > /dev/null 2>&1; then - echo "✅ Package verification successful" - - # Show package info - echo "📦 Package Information:" - dpkg-deb --field "$PACKAGE_NAME" Package Version Architecture Description | head -4 + log_success "Package verified" else - echo "❌ Package verification failed - corrupted download" - echo " File size: $(ls -lh "$PACKAGE_NAME" | awk '{print $5}')" - echo " Try downloading manually from: $PACKAGE_URL" + log_error "Package verification failed - corrupted download" rm -rf "$TEMP_DIR" exit 1 fi # Install the package -echo "" -echo "📦 Installing Regional Monitoring Agent package..." +log_info "Installing package..." if dpkg -i "$PACKAGE_NAME" 2>/dev/null; then - echo "✅ Package installed successfully" + log_success "Package installed" else - echo "⚠️ Package installation had dependency issues, attempting to fix..." + log_warning "Fixing dependencies..." if apt-get update && apt-get install -f -y; then - echo "✅ Dependencies fixed and package installed successfully" + log_success "Package installed with dependencies" else - echo "❌ Failed to install package and fix dependencies" - echo " This might be due to:" - echo " - Missing system dependencies" - echo " - Architecture compatibility issues" - echo " - Package conflicts" - echo " - Insufficient disk space" - echo "" - echo " Manual resolution:" - echo " 1. Run: sudo apt-get update" - echo " 2. Run: sudo apt-get install -f" - echo " 3. Retry installation: sudo dpkg -i $PACKAGE_NAME" + log_error "Failed to install package" + log_info "Try: sudo apt-get update && sudo apt-get install -f" rm -rf "$TEMP_DIR" exit 1 fi fi # Configure the agent -echo "" -echo "⚙️ Configuring Regional Monitoring Agent..." +log_info "Configuring agent..." # Ensure configuration directory exists mkdir -p /etc/regional-check-agent @@ -293,7 +260,6 @@ mkdir -p /etc/regional-check-agent cat > /etc/regional-check-agent/regional-check-agent.env << EOF # Distributed Regional Check Agent Configuration # Auto-generated on $(date) -# Architecture: $PKG_ARCH # Server Configuration PORT=8091 @@ -311,7 +277,7 @@ ENABLE_LOGGING=true POCKETBASE_ENABLED=true POCKETBASE_URL=$POCKETBASE_URL -# Regional Agent Configuration - Auto-configured +# Regional Agent Configuration REGION_NAME=$REGION_NAME AGENT_ID=$AGENT_ID AGENT_IP_ADDRESS=$AGENT_IP_ADDRESS @@ -323,112 +289,79 @@ MAX_RETRIES=3 REQUEST_TIMEOUT=10s EOF -echo "✅ Configuration file created at /etc/regional-check-agent/regional-check-agent.env" - # Set proper permissions if id "regional-check-agent" &>/dev/null; then chown root:regional-check-agent /etc/regional-check-agent/regional-check-agent.env chmod 640 /etc/regional-check-agent/regional-check-agent.env - echo "✅ Configuration file permissions set" else - echo "⚠️ regional-check-agent user not found, using root permissions" chown root:root /etc/regional-check-agent/regional-check-agent.env chmod 600 /etc/regional-check-agent/regional-check-agent.env fi +log_success "Configuration complete" + # Enable and start the service -echo "" -echo "🔧 Starting Regional Monitoring Agent service..." +log_info "Starting service..." # Reload systemd daemon systemctl daemon-reload # Enable the service for auto-start if systemctl enable $SERVICE_NAME; then - echo "✅ Service enabled for auto-start" + log_success "Service enabled" else - echo "❌ Failed to enable service" - echo " Check systemd configuration" + log_error "Failed to enable service" rm -rf "$TEMP_DIR" exit 1 fi # Start the service if systemctl start $SERVICE_NAME; then - echo "✅ Service started successfully" + log_success "Service started" else - echo "❌ Failed to start service" - echo " Common issues:" - echo " - Configuration errors" - echo " - Port 8091 already in use" - echo " - Permission issues" - echo "" - echo " Troubleshooting:" - echo " - Check logs: sudo journalctl -u $SERVICE_NAME -f" - echo " - Check config: sudo nano /etc/regional-check-agent/regional-check-agent.env" - echo " - Manual start: sudo systemctl start $SERVICE_NAME" + log_error "Failed to start service" + log_info "Check logs: sudo journalctl -u $SERVICE_NAME -f" rm -rf "$TEMP_DIR" exit 1 fi # Wait a moment for service to initialize -echo "⏳ Waiting for service to initialize..." sleep 5 -# Check service status -echo "" -echo "📊 Service Status:" -systemctl status $SERVICE_NAME --no-pager -l --lines=5 - # Test health endpoint -echo "" -echo "🩺 Testing agent health endpoint..." HEALTH_CHECK_ATTEMPTS=3 -HEALTH_CHECK_DELAY=2 - for i in $(seq 1 $HEALTH_CHECK_ATTEMPTS); do if curl -s -f --connect-timeout 5 http://localhost:8091/health > /dev/null; then - echo "✅ Agent health endpoint is responding" - HEALTH_OK=true + log_success "Health endpoint responding" break else if [ $i -lt $HEALTH_CHECK_ATTEMPTS ]; then - echo "⏳ Health check attempt $i/$HEALTH_CHECK_ATTEMPTS failed, retrying in ${HEALTH_CHECK_DELAY}s..." - sleep $HEALTH_CHECK_DELAY + sleep 2 else - echo "⚠️ Agent health endpoint not responding after $HEALTH_CHECK_ATTEMPTS attempts" - echo " This may be normal if the service is still starting up" - echo " Check status later with: curl http://localhost:8091/health" + log_warning "Health endpoint not responding (service may still be starting)" fi fi done # Cleanup rm -rf "$TEMP_DIR" + echo "" -echo "🎉 Regional Monitoring Agent Installation Complete!" +echo "=============================================" +echo " Installation Complete!" +echo "=============================================" echo "" -echo "📋 Installation Summary:" -echo " Agent ID: $AGENT_ID" -echo " Region: $REGION_NAME" -echo " Architecture: $PKG_ARCH ($ARCH)" -echo " Status: $(systemctl is-active $SERVICE_NAME 2>/dev/null || echo 'unknown')" -echo " Health URL: http://localhost:8091/health" -echo " Service endpoint: http://localhost:8091/operation" -echo " Config file: /etc/regional-check-agent/regional-check-agent.env" +log_success "CheckCle Regional Monitoring Agent installed successfully" echo "" -echo "📝 Useful commands:" -echo " Check status: sudo systemctl status $SERVICE_NAME" -echo " View logs: sudo journalctl -u $SERVICE_NAME -f" -echo " Restart: sudo systemctl restart $SERVICE_NAME" -echo " Stop: sudo systemctl stop $SERVICE_NAME" -echo " Health check: curl http://localhost:8091/health" +echo "Agent Details:" +echo " Region: $REGION_NAME" +echo " Agent ID: $AGENT_ID" +echo " Status: $(systemctl is-active $SERVICE_NAME 2>/dev/null || echo 'unknown')" +echo " Health: http://localhost:8091/health" echo "" -echo "🔧 Troubleshooting:" -echo " If the service fails to start:" -echo " 1. Check logs: sudo journalctl -u $SERVICE_NAME -n 50" -echo " 2. Verify config: cat /etc/regional-check-agent/regional-check-agent.env" -echo " 3. Test connectivity: ping $(echo $POCKETBASE_URL | sed 's|https\?://||' | sed 's|/.*||')" -echo " 4. Check port availability: sudo netstat -tlnp | grep 8091" +echo "Management Commands:" +echo " Status: sudo systemctl status $SERVICE_NAME" +echo " Logs: sudo journalctl -u $SERVICE_NAME -f" +echo " Restart: sudo systemctl restart $SERVICE_NAME" echo "" -echo "✨ The agent is now monitoring and reporting to your CheckCle dashboard!" \ No newline at end of file +log_success "Agent is now monitoring and reporting to your dashboard!" \ No newline at end of file From f7cf2fcc200a3340fa1961abc723727b55a4d209 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Thu, 31 Jul 2025 17:28:41 +0700 Subject: [PATCH 10/12] Fix: Set SSL initialize check with the current time. Closes: #100 - When adding a new SSL certificate, initialize with the current time. This ensures that the SSL check is triggered shortly after the certificate is added to the monitoring system. --- .../services/ssl/sslCertificateOperations.ts | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/application/src/services/ssl/sslCertificateOperations.ts b/application/src/services/ssl/sslCertificateOperations.ts index d1e87a3..aa422b0 100644 --- a/application/src/services/ssl/sslCertificateOperations.ts +++ b/application/src/services/ssl/sslCertificateOperations.ts @@ -13,6 +13,8 @@ export const addSSLCertificate = async ( certificateData: AddSSLCertificateDto ): Promise => { try { + const currentTime = new Date().toISOString(); + // Prepare the data for saving to database // The Go service will handle the actual SSL checking const data = { @@ -23,14 +25,15 @@ export const addSSLCertificate = async ( cert_sans: "", cert_alg: "", serial_number: "", - valid_from: new Date().toISOString(), // Will be updated by Go service - valid_till: new Date().toISOString(), // Will be updated by Go service + valid_from: currentTime, // Will be updated by Go service + valid_till: currentTime, // Will be updated by Go service validity_days: 0, // Will be updated by Go service days_left: 0, // Will be updated by Go service warning_threshold: Number(certificateData.warning_threshold) || 30, expiry_threshold: Number(certificateData.expiry_threshold) || 7, notification_channel: certificateData.notification_channel || "", check_interval: Number(certificateData.check_interval) || 1, // New field + check_at: currentTime, // Set to current time to trigger immediate check }; // Save to database @@ -38,7 +41,7 @@ export const addSSLCertificate = async ( return record as unknown as SSLCertificate; } catch (error) { - // console.error("Error adding SSL certificate:", error); + console.error("Error adding SSL certificate:", error); throw error; } }; @@ -67,7 +70,7 @@ export const checkAndUpdateCertificate = async ( // Return the current certificate data return typedCertificate; } catch (error) { - // console.error("Error updating SSL certificate:", error); + console.error("Error updating SSL certificate:", error); throw error; } }; @@ -84,10 +87,10 @@ export const triggerImmediateCheck = async (certificateId: string): Promise => { await pb.collection("ssl_certificates").delete(id); return true; } catch (error) { - // console.error("Error deleting SSL certificate:", error); + console.error("Error deleting SSL certificate:", error); throw error; } }; @@ -112,10 +115,10 @@ export const deleteSSLCertificate = async (id: string): Promise => { */ export const refreshAllCertificates = async (): Promise<{ success: number; failed: number }> => { try { - const response = await pb.collection("ssl_certificates").getList(1, 200); + const response = await pb.collection("ssl_certificates").getList(1, 100); const certificates = response.items as unknown as SSLCertificate[]; - // console.log(`Refreshing ${certificates.length} certificates...`); + console.log(`Refreshing ${certificates.length} certificates...`); let success = 0; let failed = 0; @@ -125,14 +128,14 @@ export const refreshAllCertificates = async (): Promise<{ success: number; faile await checkCertificateAndNotify(cert); success++; } catch (error) { - // console.error(`Failed to refresh certificate ${cert.domain}:`, error); + console.error(`Failed to refresh certificate ${cert.domain}:`, error); failed++; } } return { success, failed }; } catch (error) { - // console.error("Error refreshing certificates:", error); + console.error("Error refreshing certificates:", error); throw error; } }; \ No newline at end of file From 914f1aba608a7e97cecf311c36c811034227064e Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Sat, 2 Aug 2025 16:53:15 +0700 Subject: [PATCH 11/12] feat: Support multiple notification channels - Update the Notification Settings dashboard to allow users to select multiple notification channels for alerts. - Add webhook to notification channels: Add webhook as a selectable option in the notification channel type dropdown. - Implement template type selection in the Add Template dialog. Based on the selected type. --- .../components/servers/EditServerDialog.tsx | 42 ++- .../services/ServiceMonitoringButton.tsx | 17 +- .../alerts-templates/AlertsTemplates.tsx | 99 ++++-- .../alerts-templates/TemplateDialog.tsx | 155 +++++++-- .../alerts-templates/TemplateList.tsx | 213 ++++++------ .../form/ServerTemplateFields.tsx | 211 ++++++++++++ .../form/ServiceTemplateFields.tsx | 133 ++++++++ .../form/SslTemplateFields.tsx | 79 +++++ .../settings/alerts-templates/hooks/index.ts | 2 +- .../alerts-templates/hooks/useTemplateForm.ts | 208 +++++++----- .../NotificationChannelDialog.tsx | 317 ++++++++++++++---- .../NotificationSettings.tsx | 3 +- application/src/pages/Login.tsx | 140 +++----- .../src/services/alertConfigService.ts | 33 +- .../handlers/serviceStatusHandlers.ts | 81 +---- .../service-status/resumeMonitoring.ts | 43 +-- .../src/services/notification/index.ts | 236 +------------ .../notification/templateProcessor.ts | 26 +- .../src/services/notificationService.ts | 194 ----------- .../serverNotificationTemplateService.ts | 103 ++++++ .../serviceNotificationTemplateService.ts | 93 +++++ .../sslNotificationTemplateService.ts | 87 +++++ application/src/services/templateService.ts | 180 +++++----- application/src/services/webhookService.ts | 66 ++++ 24 files changed, 1712 insertions(+), 1049 deletions(-) create mode 100644 application/src/components/settings/alerts-templates/form/ServerTemplateFields.tsx create mode 100644 application/src/components/settings/alerts-templates/form/ServiceTemplateFields.tsx create mode 100644 application/src/components/settings/alerts-templates/form/SslTemplateFields.tsx delete mode 100644 application/src/services/notificationService.ts create mode 100644 application/src/services/serverNotificationTemplateService.ts create mode 100644 application/src/services/serviceNotificationTemplateService.ts create mode 100644 application/src/services/sslNotificationTemplateService.ts create mode 100644 application/src/services/webhookService.ts diff --git a/application/src/components/servers/EditServerDialog.tsx b/application/src/components/servers/EditServerDialog.tsx index cce5755..3db001e 100644 --- a/application/src/components/servers/EditServerDialog.tsx +++ b/application/src/components/servers/EditServerDialog.tsx @@ -1,3 +1,4 @@ + import React, { useState, useEffect } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; @@ -12,7 +13,7 @@ import { pb } from "@/lib/pocketbase"; import { Server } from "@/types/server.types"; import { RefreshCw, X } from "lucide-react"; import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService"; -import { templateService, NotificationTemplate } from "@/services/templateService"; +import { templateService, ServerNotificationTemplate } from "@/services/templateService"; import { serverThresholdService, ServerThreshold } from "@/services/serverThresholdService"; interface EditServerDialogProps { @@ -66,9 +67,9 @@ export const EditServerDialog: React.FC = ({ const [isSubmitting, setIsSubmitting] = useState(false); const [alertConfigs, setAlertConfigs] = useState([]); - const [templates, setTemplates] = useState([]); + const [templates, setTemplates] = useState([]); const [thresholds, setThresholds] = useState([]); - const [selectedTemplate, setSelectedTemplate] = useState(null); + const [selectedTemplate, setSelectedTemplate] = useState(null); const [selectedThreshold, setSelectedThreshold] = useState(null); const [loadingAlertConfigs, setLoadingAlertConfigs] = useState(false); const [loadingTemplates, setLoadingTemplates] = useState(false); @@ -78,7 +79,7 @@ export const EditServerDialog: React.FC = ({ // Initialize form data when server changes useEffect(() => { if (server) { - // console.log("Setting form data for server:", server); + // console.log("Setting form data for server:", server); // Parse comma-separated notification_id into array const notificationChannels = server.notification_id ? server.notification_id.split(',').map(id => id.trim()).filter(id => id) @@ -180,8 +181,9 @@ export const EditServerDialog: React.FC = ({ const loadTemplates = async () => { try { setLoadingTemplates(true); - const templateList = await templateService.getTemplates(); - setTemplates(templateList); + const templateList = await templateService.getTemplates('server'); + // Cast to ServerNotificationTemplate[] since we know we're getting server templates + setTemplates(templateList as ServerNotificationTemplate[]); } catch (error) { // console.error('Error loading templates:', error); toast({ @@ -621,27 +623,39 @@ export const EditServerDialog: React.FC = ({
- +

- {selectedTemplate.up_message || "No RAM threshold message defined"} + {selectedTemplate.ram_message || "No RAM message defined"}

- +

- {selectedTemplate.down_message || "No CPU threshold message defined"} + {selectedTemplate.cpu_message || "No CPU message defined"}

- +

- {selectedTemplate.incident_message || "No disk threshold message defined"} + {selectedTemplate.disk_message || "No disk message defined"}

- +

- {selectedTemplate.maintenance_message || "No network threshold message defined"} + {selectedTemplate.network_message || "No network message defined"} +

+
+
+ +

+ {selectedTemplate.up_message || "No up message defined"} +

+
+
+ +

+ {selectedTemplate.down_message || "No down message defined"}

diff --git a/application/src/components/services/ServiceMonitoringButton.tsx b/application/src/components/services/ServiceMonitoringButton.tsx index 3e2d3fc..8a0aed0 100644 --- a/application/src/components/services/ServiceMonitoringButton.tsx +++ b/application/src/components/services/ServiceMonitoringButton.tsx @@ -5,7 +5,6 @@ import { Play, Pause } from "lucide-react"; import { Service } from "@/types/service.types"; import { serviceService } from "@/services/serviceService"; import { useToast } from "@/hooks/use-toast"; -import { notificationService } from "@/services/notificationService"; interface ServiceMonitoringButtonProps { service: Service; @@ -34,16 +33,8 @@ export function ServiceMonitoringButton({ service, onStatusChange }: ServiceMoni if (onStatusChange) onStatusChange("paused"); - // Send notification for paused status (only here, not in pauseMonitoring.ts) - if (service.alerts !== "muted") { - // console.log("Sending pause notification from UI component"); - // IMPORTANT: Direct call to the notification service to ensure a message is sent - await notificationService.sendNotification({ - service: service, - status: "paused", - timestamp: new Date().toISOString(), - }); - } + // Notification handling removed - will be handled by backend + // console.log("Service paused - notifications will be handled by backend"); toast({ title: "Monitoring paused", @@ -51,7 +42,7 @@ export function ServiceMonitoringButton({ service, onStatusChange }: ServiceMoni }); } else { // Start/resume monitoring - // console.log(`Starting monitoring for service ${service.id} (${service.name})`); + // console.log(`Starting monitoring for service ${service.id} (${service.name})`); // First ensure we update the status in the database to not be paused anymore await serviceService.resumeMonitoring(service.id); @@ -100,4 +91,4 @@ export function ServiceMonitoringButton({ service, onStatusChange }: ServiceMoni )} ); -} +} \ No newline at end of file diff --git a/application/src/components/settings/alerts-templates/AlertsTemplates.tsx b/application/src/components/settings/alerts-templates/AlertsTemplates.tsx index b8226d4..de52b33 100644 --- a/application/src/components/settings/alerts-templates/AlertsTemplates.tsx +++ b/application/src/components/settings/alerts-templates/AlertsTemplates.tsx @@ -1,9 +1,10 @@ import React, { useState } from "react"; import { useQuery } from "@tanstack/react-query"; -import { templateService } from "@/services/templateService"; +import { templateService, TemplateType } from "@/services/templateService"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Plus, RefreshCcw } from "lucide-react"; import { TemplateList } from "./TemplateList"; import { TemplateDialog } from "./TemplateDialog"; @@ -11,8 +12,10 @@ import { useToast } from "@/hooks/use-toast"; export const AlertsTemplates = () => { const { toast } = useToast(); + const [activeTab, setActiveTab] = useState('service'); const [isDialogOpen, setIsDialogOpen] = useState(false); const [editingTemplate, setEditingTemplate] = useState(null); + const [editingTemplateType, setEditingTemplateType] = useState(null); const { data: templates = [], @@ -20,17 +23,19 @@ export const AlertsTemplates = () => { error, refetch } = useQuery({ - queryKey: ['notification_templates'], - queryFn: templateService.getTemplates, + queryKey: ['notification_templates', activeTab], + queryFn: () => templateService.getTemplates(activeTab), }); - const handleAddTemplate = () => { + const handleAddTemplate = (templateType: TemplateType) => { setEditingTemplate(null); + setEditingTemplateType(templateType); setIsDialogOpen(true); }; - const handleEditTemplate = (id: string) => { + const handleEditTemplate = (id: string, templateType: TemplateType) => { setEditingTemplate(id); + setEditingTemplateType(templateType); setIsDialogOpen(true); }; @@ -51,33 +56,83 @@ export const AlertsTemplates = () => { Refresh - - {error ? ( -
-

Error loading templates

- -
- ) : ( - - )} + setActiveTab(value as TemplateType)}> + + Service Uptime + Server Monitoring + SSL Certificate + + + + {error ? ( +
+

Error loading service templates

+ +
+ ) : ( + handleEditTemplate(id, 'service')} + refetchTemplates={refetch} + templateType="service" + /> + )} +
+ + + {error ? ( +
+

Error loading server templates

+ +
+ ) : ( + handleEditTemplate(id, 'server')} + refetchTemplates={refetch} + templateType="server" + /> + )} +
+ + + {error ? ( +
+

Error loading SSL templates

+ +
+ ) : ( + handleEditTemplate(id, 'ssl')} + refetchTemplates={refetch} + templateType="ssl" + /> + )} +
+
{ refetch(); @@ -86,4 +141,4 @@ export const AlertsTemplates = () => { /> ); -}; +}; \ No newline at end of file diff --git a/application/src/components/settings/alerts-templates/TemplateDialog.tsx b/application/src/components/settings/alerts-templates/TemplateDialog.tsx index 4b9b13d..a1afeb7 100644 --- a/application/src/components/settings/alerts-templates/TemplateDialog.tsx +++ b/application/src/components/settings/alerts-templates/TemplateDialog.tsx @@ -1,19 +1,25 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; -import { Form } from "@/components/ui/form"; +import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useTemplateForm } from "./hooks/useTemplateForm"; -import { BasicTemplateFields } from "./form/BasicTemplateFields"; -import { MessagesTabContent } from "./form/MessagesTabContent"; -import { PlaceholdersTabContent } from "./form/PlaceholdersTabContent"; +import { ServerTemplateFields } from "./form/ServerTemplateFields"; +import { ServiceTemplateFields } from "./form/ServiceTemplateFields"; +import { SslTemplateFields } from "./form/SslTemplateFields"; import { Loader2, ChevronDown } from "lucide-react"; import { ScrollArea } from "@/components/ui/scroll-area"; +import { TemplateType, templateTypeConfigs } from "@/services/templateService"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Textarea } from "@/components/ui/textarea"; interface TemplateDialogProps { open: boolean; templateId: string | null; + templateType: TemplateType | null; onOpenChange: (open: boolean) => void; onSuccess: () => void; } @@ -21,9 +27,12 @@ interface TemplateDialogProps { export const TemplateDialog: React.FC = ({ open, templateId, + templateType: initialTemplateType, onOpenChange, onSuccess, }) => { + const [selectedTemplateType, setSelectedTemplateType] = useState(initialTemplateType || 'service'); + const { form, isEditMode, @@ -32,28 +41,70 @@ export const TemplateDialog: React.FC = ({ onSubmit } = useTemplateForm({ templateId, + templateType: selectedTemplateType, open, onOpenChange, onSuccess }); - // For debugging purposes + // Update template type when prop changes or dialog opens useEffect(() => { - if (open) { - // console.log("Template dialog opened. Edit mode:", isEditMode, "Template ID:", templateId); - - // Log form values when they change - const subscription = form.watch((value) => { - // console.log("Current form values:", value); - }); - - return () => subscription.unsubscribe(); + if (initialTemplateType) { + setSelectedTemplateType(initialTemplateType); + } else if (open && !isEditMode) { + setSelectedTemplateType('service'); } - }, [open, isEditMode, templateId, form]); + }, [initialTemplateType, open, isEditMode]); + + // Handle template type change + const handleTemplateTypeChange = (newType: TemplateType) => { + if (!isEditMode) { + setSelectedTemplateType(newType); + form.setValue('templateType', newType); + } + }; + + const renderTemplateFields = () => { + switch (selectedTemplateType) { + case 'server': + return ; + case 'service': + return ; + case 'ssl': + return ; + default: + return null; + } + }; + + const renderPlaceholderGuide = () => { + const config = templateTypeConfigs[selectedTemplateType]; + if (!config) return null; + + return ( + + + Available Placeholders + + +

+ {config.description}. Use these placeholders in your messages: +

+
+ {config.placeholders.map((placeholder) => ( +
+ {placeholder} +
+ ))} +
+
+
+ ); + }; return ( - + {isEditMode ? "Edit Template" : "Add Template"} @@ -69,7 +120,69 @@ export const TemplateDialog: React.FC = ({
- + {/* Basic Fields */} +
+ ( + + Template Name + + + + + + )} + /> + + ( + + Template Type + + + + + + )} + /> + + ( + + Custom Placeholder + + + + + + )} + /> +
@@ -78,11 +191,11 @@ export const TemplateDialog: React.FC = ({ - + {renderTemplateFields()} - + {renderPlaceholderGuide()}
@@ -120,4 +233,4 @@ export const TemplateDialog: React.FC = ({
); -}; +}; \ No newline at end of file diff --git a/application/src/components/settings/alerts-templates/TemplateList.tsx b/application/src/components/settings/alerts-templates/TemplateList.tsx index e3c40d2..d820e96 100644 --- a/application/src/components/settings/alerts-templates/TemplateList.tsx +++ b/application/src/components/settings/alerts-templates/TemplateList.tsx @@ -1,17 +1,15 @@ import React, { useState } from "react"; -import { NotificationTemplate, templateService } from "@/services/templateService"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; import { Button } from "@/components/ui/button"; -import { Edit, Trash2 } from "lucide-react"; -import { +import { Badge } from "@/components/ui/badge"; +import { Trash2, Edit, MoreVertical } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { AlertDialog, AlertDialogAction, AlertDialogCancel, @@ -22,63 +20,75 @@ import { AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { useToast } from "@/hooks/use-toast"; -import { Badge } from "@/components/ui/badge"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { templateService, AnyTemplate, TemplateType } from "@/services/templateService"; interface TemplateListProps { - templates: NotificationTemplate[]; + templates: AnyTemplate[]; isLoading: boolean; onEdit: (id: string) => void; refetchTemplates: () => void; + templateType: TemplateType; } -export const TemplateList: React.FC = ({ - templates, - isLoading, - onEdit, +export const TemplateList: React.FC = ({ + templates, + isLoading, + onEdit, refetchTemplates, + templateType }) => { const { toast } = useToast(); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [templateToDelete, setTemplateToDelete] = useState(null); - const [isDeleting, setIsDeleting] = useState(false); + const queryClient = useQueryClient(); + const [deleteTemplateId, setDeleteTemplateId] = useState(null); - const handleDeletePrompt = (template: NotificationTemplate) => { - setTemplateToDelete(template); - setDeleteDialogOpen(true); - }; - - const handleDeleteTemplate = async () => { - if (!templateToDelete) return; - - setIsDeleting(true); - try { - await templateService.deleteTemplate(templateToDelete.id); + // Delete mutation + const deleteMutation = useMutation({ + mutationFn: (id: string) => templateService.deleteTemplate(id, templateType), + onSuccess: () => { toast({ title: "Template deleted", - description: `Template "${templateToDelete.name}" has been removed.`, + description: "The template has been deleted successfully.", }); + queryClient.invalidateQueries({ queryKey: ['notification_templates', templateType] }); refetchTemplates(); - } catch (error) { - console.error("Error deleting template:", error); + }, + onError: (error) => { + // console.error("Error deleting template:", error); toast({ title: "Error", description: "Failed to delete template. Please try again.", variant: "destructive", }); - } finally { - setIsDeleting(false); - setDeleteDialogOpen(false); - setTemplateToDelete(null); + }, + }); + + const handleDelete = (id: string) => { + setDeleteTemplateId(id); + }; + + const confirmDelete = () => { + if (deleteTemplateId) { + deleteMutation.mutate(deleteTemplateId); + setDeleteTemplateId(null); } }; if (isLoading) { return ( -
-
-
-
-
+
+ {[...Array(3)].map((_, i) => ( +
+
+
+
+
+
+
+
+
+
+ ))}
); } @@ -86,83 +96,86 @@ export const TemplateList: React.FC = ({ if (templates.length === 0) { return (
-

No templates found

-

- Create your first notification template to get started. -

+

No templates found. Create your first template to get started.

); } + const getTemplateTypeLabel = (type: TemplateType) => { + switch (type) { + case 'server': return 'Server'; + case 'service': return 'Service'; + case 'ssl': return 'SSL'; + default: return 'Unknown'; + } + }; + return ( <> -
- - - - Name - Type - Created - Actions - - - - {templates.map((template) => ( - - {template.name} - - {template.type} - - - {new Date(template.created).toLocaleDateString()} - - -
- - -
-
-
- ))} -
-
+
+ {templates.map((template) => ( +
+
+
+

{template.name}

+ {getTemplateTypeLabel(templateType)} +
+

+ Created: {new Date(template.created).toLocaleDateString()} + {template.updated !== template.created && + ` • Updated: ${new Date(template.updated).toLocaleDateString()}` + } +

+
+
+ + + + + + + handleDelete(template.id)} + className="text-destructive focus:text-destructive" + > + + Delete + + + +
+
+ ))}
- + setDeleteTemplateId(null)}> - Delete Template + Are you sure? - Are you sure you want to delete the template "{templateToDelete?.name}"? This action cannot be undone. + This action cannot be undone. This will permanently delete the template. - Cancel + Cancel { - e.preventDefault(); - handleDeleteTemplate(); - }} - disabled={isDeleting} + onClick={confirmDelete} className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > - {isDeleting ? "Deleting..." : "Delete"} + Delete ); -}; +}; \ No newline at end of file diff --git a/application/src/components/settings/alerts-templates/form/ServerTemplateFields.tsx b/application/src/components/settings/alerts-templates/form/ServerTemplateFields.tsx new file mode 100644 index 0000000..259a2e4 --- /dev/null +++ b/application/src/components/settings/alerts-templates/form/ServerTemplateFields.tsx @@ -0,0 +1,211 @@ + +import React from "react"; +import { FormField, FormItem, FormLabel, FormControl, FormMessage, FormDescription } from "@/components/ui/form"; +import { Textarea } from "@/components/ui/textarea"; +import { Control } from "react-hook-form"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +interface ServerTemplateFieldsProps { + control: Control; +} + +export const ServerTemplateFields: React.FC = ({ control }) => { + return ( +
+ + + System Resource Messages + + +
+ ( + + CPU Alert Message + +