Guacamole Prompt

Panduan Kustomisasi & Branding Premium Apache Guacamole dengan Antigravity (agy)

Halo teman-teman IT! Berikut adalah panduan dan master prompt lengkap untuk melakukan kustomisasi visual Apache Guacamole agar terlihat modern, bersih (*clean look*), dan profesional. Panduan ini dirancang untuk dijalankan oleh agen kecerdasan buatan Antigravity CLI (agy) secara otomatis di server baru yang masih bersih (hanya terinstall OS saja).

⚠️ Prasyarat Utama: Sebelum meminta agy menjalankan panduan ini di server baru, pastikan Anda telah menaruh file gambar logo resmi instansi/aplikasi Anda di direktori: /root/install/images/logo.png.

Master Prompt Kustomisasi Guacamole

Salin seluruh isi berkas teks di bawah ini ke dalam sebuah file di server baru Anda (misalnya dinamai /root/install/guacamole_prompt.md). Setelah itu, Anda cukup memerintahkan agy untuk mengeksekusinya.

# MASTER PROMPT: CUSTOM BRANDING CONFIGURATION FOR APACHE GUACAMOLE

Please read and execute the following step-by-step instructions to configure Apache Guacamole on a fresh server with custom branding and a polished Indonesian translation system.

---

### **Configuration Variables**
Before starting, identify the target values for the deployment (edit these in the scripts/files below as needed):
1. **Application Name (Title)**: RDP GATEWAY
2. **Municipality Name (Subtitle)**: SERVER BANG-BEN
3. **Logo Image**: The target logo PNG file must be placed at `/root/install/images/logo.png` beforehand.

---

### **Step 1: Setup Directories**
Create the following directory structure:
```bash
mkdir -p /root/install/images /root/install/translations
```

---

### **Step 2: Generate and Correct Translations (id.json & en.json)**
Create a Python script `/root/install/translate_guac.py` with the following content. 

This script:
1. Loads the default English translation file (`en.json`) from the Tomcat directory.
2. Scans for any active database auth extension JAR (e.g., MySQL/MariaDB extension `guacamole-auth-jdbc-*.jar`) in `/etc/guacamole/extensions/` and automatically merges its translation strings using Python's native `zipfile` module.
3. Automatically **skips translation** for technical IT configuration and protocol namespaces (such as `MANAGE_CONNECTION`, `MANAGE_USER`, and all `PROTOCOL_*` fields) to keep them in English. This prevents confusing literal translations of technical terms like "server" becoming "terpencil" or "port" becoming "pelabuhan".
4. Translates the remaining general UI terms using a lightweight Google Translate endpoint.
5. Fixes common automated translation mistakes (e.g. "tetikus" -> "mouse", "tuan rumah" -> "host").
6. Restores AngularJS ICU syntax structures (`select` and `plural`) and CSV/JSON/YAML import templates to their original formats.
7. Saves the output to both `/root/install/translations/id.json` and `/root/install/translations/en.json` (to force the Indonesian translations across all user browsers).

```python
import json
import urllib.request
import urllib.parse
import re
import time
import sys
import os
import glob
import zipfile

# ==========================================
# CONFIGURATION VARIABLES
# ==========================================
APP_NAME = "RDP GATEWAY"
# ==========================================

def translate_text(text, target_lang='id', source_lang='en'):
    if not text.strip() or text.startswith('@:'):
        return text
    
    # Extract ICU placeholders to avoid translation corruption
    placeholders = re.findall(r'\{[a-zA-Z0-9_-]+\}', text)
    temp_text = text
    for i, placeholder in enumerate(placeholders):
        temp_text = temp_text.replace(placeholder, f" __{i}__ ")
        
    url = "https://translate.googleapis.com/translate_a/single"
    params = {
        'client': 'gtx',
        'sl': source_lang,
        'tl': target_lang,
        'dt': 't',
        'q': temp_text
    }
    
    encoded_params = urllib.parse.urlencode(params)
    full_url = f"{url}?{encoded_params}"
    
    req = urllib.request.Request(
        full_url, 
        headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}
    )
    
    try:
        with urllib.request.urlopen(req) as response:
            res_data = json.loads(response.read().decode('utf-8'))
            translated = "".join([part[0] for part in res_data[0] if part[0]])
            
            # Restore ICU placeholders
            for i, placeholder in enumerate(placeholders):
                translated = re.sub(rf'\s*__\s*{i}\s*__\s*', f" {placeholder} ", translated)
            
            translated = re.sub(r'\s+', ' ', translated).strip()
            if text.startswith(' ') and not translated.startswith(' '):
                translated = ' ' + translated
            if text.endswith(' ') and not translated.endswith(' '):
                translated = translated + ' '
                
            return translated
    except Exception as e:
        print(f"Error translating '{text}': {e}", file=sys.stderr)
        return text

def fix_text(text):
    if not isinstance(text, str):
        return text
    
    # Precise replacements for IT/technical term mistranslations
    text = re.sub(r'\btetikus\b', 'mouse', text, flags=re.IGNORECASE)
    text = re.sub(r'\btuan\s+rumah\b', 'host', text, flags=re.IGNORECASE)
    text = re.sub(r'\bpelabuhan\b', 'port', text, flags=re.IGNORECASE)
    text = re.sub(r'\bterpencil\b', 'remote', text, flags=re.IGNORECASE)
    text = re.sub(r'\bbunuh\b', 'akhiri', text, flags=re.IGNORECASE)
    text = re.sub(r'\bmematikan\b', 'menghentikan', text, flags=re.IGNORECASE)
    text = re.sub(r'\bmembunuh\b', 'mengakhiri', text, flags=re.IGNORECASE)
    text = re.sub(r'\bjalur\s+berkendara\b', 'path drive', text, flags=re.IGNORECASE)
    text = re.sub(r'\bnama\s+penggerak\b', 'nama drive', text, flags=re.IGNORECASE)
    text = re.sub(r'\baktifkan\s+penggerak\b', 'aktifkan drive', text, flags=re.IGNORECASE)
    text = re.sub(r'\bjalur\s+penggerak\b', 'path drive', text, flags=re.IGNORECASE)
    text = re.sub(r'\bnaskah\s+\(perekaman\s+sesi\s+teks\)', 'Typescript (Perekaman Sesi Teks)', text, flags=re.IGNORECASE)
    text = re.sub(r'\bnama\s+naskah\b', 'nama typescript', text, flags=re.IGNORECASE)
    text = re.sub(r'\bjalur\s+skrip\s+ketikan\b', 'path typescript', text, flags=re.IGNORECASE)
    text = re.sub(r'\bjalur\s+naskah\b', 'path typescript', text, flags=re.IGNORECASE)
    text = re.sub(r'\bspasi\s+mundur\b', 'backspace', text, flags=re.IGNORECASE)
    return text

def translate_dict(d, cache, skip_namespaces, path=''):
    new_dict = {}
    for k, v in d.items():
        curr_path = f"{path}.{k}" if path else k
        
        # Check if namespace/path should remain in English to avoid IT mistranslation
        is_skipped = False
        for skip_ns in skip_namespaces:
            if curr_path == skip_ns or curr_path.startswith(skip_ns + "."):
                is_skipped = True
                break
        
        if curr_path.startswith("PROTOCOL_"):
            is_skipped = True
            
        if is_skipped:
            # Keep original English
            new_dict[k] = v
            continue
            
        if isinstance(v, dict):
            new_dict[k] = translate_dict(v, cache, skip_namespaces, curr_path)
        elif isinstance(v, str):
            if v in cache:
                new_dict[k] = cache[v]
            else:
                translated = translate_text(v)
                corrected = fix_text(translated)
                cache[v] = corrected
                new_dict[k] = corrected
                print(f"Translated: '{v}' -> '{corrected}'")
                time.sleep(0.02)
        else:
            new_dict[k] = v
    return new_dict

def main():
    print("Loading English translation file...")
    webapps_path = "/opt/tomcat/webapps"
    en_file = os.path.join(webapps_path, "guacamole/translations/en.json")
    if not os.path.exists(en_file):
        en_file = os.path.join(webapps_path, "ROOT/translations/en.json")
        
    with open(en_file, 'r') as f:
        data = json.load(f)
        
    # Extract and merge JDBC translations if present
    jdbc_jars = glob.glob("/etc/guacamole/extensions/guacamole-auth-jdbc-*.jar")
    if jdbc_jars:
        jdbc_jar = jdbc_jars[0]
        try:
            with zipfile.ZipFile(jdbc_jar, 'r') as z:
                jdbc_en_content = z.read("org/apache/guacamole/auth/jdbc/translations/en.json")
                jdbc_data = json.loads(jdbc_en_content.decode('utf-8'))
                for k, v in jdbc_data.items():
                    if k in data and isinstance(data[k], dict) and isinstance(v, dict):
                        data[k].update(v)
                    else:
                        data[k] = v
                print(f"Successfully loaded and merged JDBC translations from {jdbc_jar}")
        except Exception as e:
            print(f"Warning: Could not merge JDBC translations: {e}")
            
    # Define namespaces to be kept in English (e.g., configurations, protocols, fields)
    skip_namespaces = [
        "MANAGE_CONNECTION",
        "MANAGE_CONNECTION_GROUP",
        "CONNECTION_ATTRIBUTES",
        "CONNECTION_GROUP_ATTRIBUTES",
        "MANAGE_USER",
        "MANAGE_USER_GROUP",
        "USER_ATTRIBUTES"
    ]
    
    cache = {}
    print("Translating strings to Indonesian (excluding technical fields)...")
    translated_data = translate_dict(data, cache, skip_namespaces)
    
    # Custom Action Buttons & Technical Term Overrides
    overrides = {
        "APP.ACTION_CLEAR": "Bersihkan",
        "APP.ACTION_DELETE": "Hapus",
        "APP.ACTION_DELETE_SESSIONS": "Akhiri Sesi",
        "APP.ACTION_PAUSE": "Jeda",
        "APP.ACTION_PLAY": "Putar",
        "APP.ACTION_SAVE": "Simpan",
        "APP.ACTION_SEARCH": "Cari",
        "APP.ACTION_VIEW_HISTORY": "Riwayat",
        "APP.ACTION_VIEW_RECORDING": "Lihat",
        "APP.ACTION_NAVIGATE_HOME": "Beranda",
        "APP.ACTION_CANCEL": "Batalkan",
        "APP.ACTION_CLONE": "Gandakan",
        "APP.ACTION_CONTINUE": "Lanjutkan",
        "APP.ACTION_SHARE": "Bagikan",
        "APP.FIELD_PLACEHOLDER_FILTER": "Filter",
        "CLIENT.ACTION_DISCONNECT": "Putus Koneksi",
        
        "LOGIN.FIELD_HEADER_USERNAME": "Nama Pengguna",
        "SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_USERNAME": "Nama Pengguna",
        "SETTINGS_PREFERENCES.FIELD_HEADER_USERNAME": "Nama Pengguna:",
        "SETTINGS_USERS.TABLE_HEADER_USERNAME": "Nama Pengguna",
        "SETTINGS_SESSIONS.TABLE_HEADER_SESSION_USERNAME": "Nama Pengguna",

        # Fix ICU template string syntax (preventing syntax errors)
        "APP.INFO_ACTIVE_USER_COUNT": "Saat ini digunakan oleh {USERS} {USERS, plural, one {pengguna} other {pengguna} }.",
        "APP.TEXT_HISTORY_DURATION": "{VALUE} {UNIT, select, second{{VALUE, plural, one{detik} other{detik}}} minute{{VALUE, plural, one{menit} other{menit}}} hour{{VALUE, plural, one{jam} other{jam}}} day{{VALUE, plural, one{hari} other{hari}}} other{}}",
        "CLIENT.HELP_SHARE_LINK": "Koneksi saat ini sedang dibagikan, dan dapat diakses oleh siapa saja dengan {LINKS, plural, one {tautan} other {tautan} } berikut:",
        "CLIENT.INFO_ANONYMOUS_USER_COUNT": "Anonim{COUNT, plural, one{} other{ (#)}}",
        "CLIENT.INFO_USER_COUNT": "{USERNAME} {COUNT, plural, one{} other{ (#)}}",
        "CLIENT.TEXT_RECONNECT_DOWN": "Menyambung kembali dalam {REMAINING} {REMAINING, plural, one {detik} other {detik} }...",
        "CLIENT.TEXT_FILE_TRANSFER_PROGRESS": "{PROGRESS} {UNIT, select, b {B} kb {KB} mb {MB} gb {GB} other{}}",
        "IMPORT.INFO_CONNECTIONS_IMPORTED_SUCCESS": "{NUMBER} {NUMBER, plural, one {koneksi} other {koneksi} } berhasil diimpor.",
        "PLAYER.INFO_NUMBER_OF_RESULTS": "{RESULTS} {RESULTS, plural, one {kecocokan} other {kecocokan} }",

        # Keep original English import template keys/formats
        "IMPORT.HELP_CSV_EXAMPLE": "name,protocol,username,password,hostname,group,users,groups,guacd-encryption (attribute)\nconn1,vnc,alice,pass1,conn1.web.com,ROOT,guac user 1;guac user 2,Connection 1 Users,none\nconn2,rdp,bob,pass2,conn2.web.com,ROOT/Parent Group,guac user 1,,ssl\nconn3,ssh,carol,pass3,conn3.web.com,ROOT/Parent Group/Child Group,guac user 2;guac user 3,,\nconn4,kubernetes,,,,,,,",
        "IMPORT.HELP_JSON_EXAMPLE": "[\n  {\n    \"name\": \"conn1\",\n    \"protocol\": \"vnc\",\n    \"parameters\": { \"username\": \"alice\", \"password\": \"pass1\", \"hostname\": \"conn1.web.com\" },\n    \"parentIdentifier\": \"ROOT\",\n    \"users\": [ \"guac user 1\", \"guac user 2\" ],\n    \"groups\": [ \"Connection 1 Users\" ],\n    \"attributes\": { \"guacd-encryption\": \"none\" }\n  },\n  {\n    \"name\": \"conn2\",\n    \"protocol\": \"rdp\",\n    \"parameters\": { \"username\": \"bob\", \"password\": \"pass2\", \"hostname\": \"conn2.web.com\" },\n    \"group\": \"ROOT/Parent Group\",\n    \"users\": [ \"guac user 1\" ],\n    \"attributes\": { \"guacd-encryption\": \"none\" }\n  },\n  {\n    \"name\": \"conn3\",\n    \"protocol\": \"ssh\",\n    \"parameters\": { \"username\": \"carol\", \"password\": \"pass3\", \"hostname\": \"conn3.web.com\" },\n    \"group\": \"ROOT/Parent Group/Child Group\",\n    \"users\": [ \"guac user 2\", \"guac user 3\" ]\n  },\n  {\n    \"name\": \"conn4\",\n    \"protocol\": \"kubernetes\"\n  }\n]",
        "IMPORT.HELP_YAML_EXAMPLE": "---\n  - name: conn1\n    protocol: vnc\n    parameters:\n      username: alice\n      password: pass1\n      hostname: conn1.web.com\n    group: ROOT\n    users:\n      - guac user 1\n      - guac user 2\n    groups:\n    - Connection 1 Users\n    attributes:\n      guacd-encryption: none\n  - name: conn2\n    protocol: rdp\n    parameters:\n      username: bob\n      password: pass2\n      hostname: conn2.web.com\n    group: ROOT/Parent Group\n    users:\n      - guac user 1\n    attributes:\n      guacd-encryption: none\n  - name: conn3\n    protocol: ssh\n    parameters:\n      username: carol\n      password: pass3\n      hostname: conn3.web.com\n    group: ROOT/Parent Group/Child Group\n    users:\n      - guac user 2\n      - guac user 3\n  - name: conn4\n    protocol: kubernetes",
        "IMPORT.HELP_SEMICOLON_FOOTNOTE": "Jika ada, titik koma dapat di-escape menggunakan backslash, contoh: \"first\\;last\"",
    }
    
    # Apply overrides
    for path, val in overrides.items():
        parts = path.split('.')
        curr = translated_data
        for part in parts[:-1]:
            if part not in curr:
                curr[part] = {}
            curr = curr[part]
        curr[parts[-1]] = val
        
    # Title & Placeholders
    translated_data['APP']['NAME'] = APP_NAME
    translated_data['LOGIN']['FIELD_PLACEHOLDER_USERNAME'] = "Nama Pengguna"
    translated_data['LOGIN']['FIELD_PLACEHOLDER_PASSWORD'] = "Kata Sandi"
    
    print("Saving translations/id.json...")
    translated_data['NAME'] = "Bahasa Indonesia"
    with open("/root/install/translations/id.json", 'w') as f:
        json.dump(translated_data, f, indent=4, ensure_ascii=False)
        
    print("Saving translations/en.json...")
    translated_data['NAME'] = "English"
    with open("/root/install/translations/en.json", 'w') as f:
        json.dump(translated_data, f, indent=4, ensure_ascii=False)
        
    print("Translation completed successfully!")

if __name__ == "__main__":
    main()
```

Run this python script:
```bash
python3 /root/install/translate_guac.py
```

---

### **Step 3: Write Guacamole Manifest (`guac-manifest.json`)**
Write `/root/install/guac-manifest.json` as follows:
```json
{
    "guacamoleVersion" : "1.6.0",
    "name"             : "Custom Pemkab Branding",
    "namespace"        : "pemkab-branding",
    "css" : [
        "app.css"
    ],
    "js" : [
        "patch-templates.js"
    ],
    "translations" : [
        "translations/en.json",
        "translations/id.json"
    ],
    "resources" : {
        "images/logo.png" : "image/png",
        "images/logo-64.png" : "image/png",
        "images/logo-144.png" : "image/png"
    }
}
```

---

### **Step 4: Write AngularJS Patches (`patch-templates.js`)**
Write `/root/install/patch-templates.js` to modify the login templates and form placeholders in AngularJS's `$templateCache`.

Customize the variables at the top of the file as needed.

```javascript
angular.module('index').run(['$templateCache', function($templateCache) {
    
    // --- CONFIGURATION VARIABLES ---
    var APP_NAME = "RDP GATEWAY";
    var SUBTITLE = "SERVER BANG-BEN";
    // -------------------------------
    
    // Patch usernameField placeholder
    $templateCache.put('app/form/templates/usernameField.html',
        '<div class="username-field">\n' +
        '    <input type="text"\n' +
        '           ng-attr-id="{{ fieldId }}"\n' +
        '           ng-attr-name="{{ field.name }}"\n' +
        '           ng-model="model"\n' +
        '           ng-disabled="disabled"\n' +
        '           guac-focus="focused"\n' +
        '           autocorrect="off"\n' +
        '           autocapitalize="off"\n' +
        '           placeholder="{{ \'LOGIN.FIELD_PLACEHOLDER_USERNAME\' | translate }}">\n' +
        '</div>'
    );
    
    // Patch passwordField placeholder
    $templateCache.put('app/form/templates/passwordField.html',
        '<div class="password-field">\n' +
        '    <input type="password"\n' +
        '           ng-disabled="disabled"\n' +
        '           ng-attr-id="{{ fieldId }}"\n' +
        '           ng-attr-name="{{ field.name }}"\n' +
        '           ng-model="model"\n' +
        '           ng-trim="false"\n' +
        '           guac-focus="focused"\n' +
        '           autocorrect="off"\n' +
        '           autocapitalize="off"\n' +
        '           placeholder="{{ \'LOGIN.FIELD_PLACEHOLDER_PASSWORD\' | translate }}">\n' +
        '</div>'
    );

    // Patch login.html for split-screen custom layout (Branding on Left, Form on Right, added Admin Notice)
    $templateCache.put('app/login/templates/login.html',
        '<div class="login-ui" ng-class="{error: loginError, continuation: isContinuation(), initial: !isContinuation()}">\n' +
        '    <p class="login-error" translate="{{loginError.key}}" translate-values="{{loginError.variables}}"></p>\n' +
        '    <div class="login-dialog-middle">\n' +
        '        <div class="login-dialog notification custom-split-layout">\n' +
        '            <!-- LEFT PANEL (BRANDING ONLY) -->\n' +
        '            <div class="login-left-panel">\n' +
        '                <div class="logo"></div>\n' +
        '                <div class="brand-title">' + APP_NAME + '</div>\n' +
        '                <div class="brand-subtitle">' + SUBTITLE + '</div>\n' +
        '            </div>\n' +
        '            <!-- RIGHT PANEL (FORM ONLY) -->\n' +
        '            <div class="login-right-panel">\n' +
        '                <form class="login-form" ng-submit="login()">\n' +
        '                    <h2 class="login-title">LOGIN</h2>\n' +
        '                    <div class="login-fields">\n' +
        '                        <guac-form namespace="\'LOGIN\'" content="remainingFields" model="enteredValues" focused="relevantField.name" data-disabled="submitted"></guac-form>\n' +
        '                    </div>\n' +
        '                    <div class="buttons">\n' +
        '                        <input type="submit" name="login" class="login" ng-disabled="submitted" value="{{\'LOGIN.ACTION_LOGIN\' | translate}}">\n' +
        '                        <input type="submit" name="login" class="continue-login" ng-disabled="submitted" value="{{\'LOGIN.ACTION_CONTINUE\' | translate}}">\n' +
        '                    </div>\n' +
        '                    <div class="admin-notice">Mintalah User dan Password kepada Admin, untuk bisa menggunakan aplikasi ini</div>\n' +
        '                </form>\n' +
        '            </div>\n' +
        '        </div>\n' +
        '    </div>\n' +
        '</div>'
    );
    
}]);
```

---

### **Step 5: Write Custom CSS (`app.css`)**
Write `/root/install/app.css` containing the custom styling layout system:
```css
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&display=swap');

/* --- 1. KEYFRAMES FOR BACKGROUND ANIMATION --- */
@keyframes gradientBG { 
    0% { background-position: 0% 50%; } 
    50% { background-position: 100% 50%; } 
    100% { background-position: 0% 50%; } 
}

/* --- 2. LOGIN PAGE STYLES (SPLIT SCREEN LIGHT VIOLET THEME) --- */
.login-ui { 
    background: linear-gradient(135deg, #c3cbd6, #eef2f7) !important; 
    font-family: 'Outfit', sans-serif !important; 
    min-height: 100vh !important; 
    display: flex !important;
    flex-direction: column !important;
    align-items: center !important;
    justify-content: center !important;
    width: 100% !important;
}

.login-dialog-middle {
    display: flex !important;
    align-items: center !important;
    justify-content: center !important;
    width: 100% !important;
}

.login-dialog.custom-split-layout { 
    background-color: #ffffff !important; 
    background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' opacity='0.022'/%3E%3C/svg%3E") !important;
    border: 1px solid rgba(103, 61, 230, 0.15) !important; 
    border-radius: 28px !important; 
    box-shadow: 
        0 8px 16px rgba(103, 61, 230, 0.04),
        0 25px 60px rgba(29, 30, 32, 0.12), 
        0 2px 4px rgba(0, 0, 0, 0.05) !important; 
    padding: 0 !important; 
    width: 780px !important; 
    max-width: 95% !important; 
    height: 480px !important;
    display: flex !important;
    flex-direction: row !important;
    overflow: hidden !important;
    margin: 0 auto !important;
    transition: all 0.3s ease !important;
}

/* Sembunyikan Versi dan Label Teks Asli */
.login-dialog .version { display: none !important; }
.login-dialog .labeled-field label { display: none !important; }

/* LEFT PANEL (BRANDING ONLY) */
.login-left-panel {
    width: 45% !important;
    background: linear-gradient(135deg, #8c85ff, #673de6) !important; 
    border-top-right-radius: 200px 300px !important;
    border-bottom-right-radius: 200px 300px !important;
    display: flex !important;
    flex-direction: column !important;
    justify-content: center !important;
    align-items: center !important;
    padding: 2.5rem 2rem !important;
    box-sizing: border-box !important;
    position: relative !important;
    box-shadow: 5px 0 15px rgba(0,0,0,0.08) !important;
}

.login-left-panel .logo {
    width: 110px !important;
    height: 125px !important;
    margin: 0 auto 20px auto !important; 
    background-image: url('app/ext/pemkab-branding/images/logo.png') !important;
    background-size: contain !important;
    background-repeat: no-repeat !important;
    background-position: center !important;
    filter: drop-shadow(0 6px 12px rgba(0,0,0,0.25)) !important;
}

.login-left-panel .brand-title {
    color: #facc15 !important; /* Gold */
    font-size: 21px !important;
    font-weight: 800 !important;
    white-space: nowrap !important;
    margin-top: 10px !important;
    letter-spacing: 1px !important;
    text-shadow: 0 2px 4px rgba(0,0,0,0.3) !important;
    text-align: center !important;
}

.login-left-panel .brand-subtitle {
    color: #ffffff !important; /* White */
    font-size: 13px !important;
    font-weight: 600 !important;
    white-space: nowrap !important;
    margin-top: 6px !important;
    letter-spacing: 1.5px !important;
    text-shadow: 0 2px 4px rgba(0,0,0,0.3) !important;
    text-align: center !important;
}

/* RIGHT PANEL (FORM) */
.login-right-panel {
    width: 55% !important;
    background: transparent !important;
    display: flex !important;
    flex-direction: column !important;
    justify-content: center !important;
    padding: 2.5rem 3rem !important;
    box-sizing: border-box !important;
}

.login-right-panel .login-form {
    width: 100% !important;
    margin: 0 !important;
}

.login-title {
    font-size: 24px !important;
    font-weight: 800 !important;
    color: #1d1e20 !important;
    margin: 0 auto 25px auto !important;
    text-align: center !important;
    letter-spacing: 0.5px !important;
}

/* Kotak Input Terang & Melengkung Penuh di Login */
.login-dialog .login-fields .username-field input,
.login-dialog .login-fields .password-field input { 
    background-color: #eef2f7 !important; 
    border: 1px solid transparent !important; 
    color: #1d1e20 !important; 
    border-radius: 50px !important; 
    padding: 12px 24px !important; 
    text-align: left !important; 
    width: 100% !important; 
    box-sizing: border-box !important; 
    margin-bottom: 12px !important; 
    font-size: 14px !important; 
    transition: all 0.3s ease !important;
}

.login-dialog .login-fields input[type="text"]:focus, 
.login-dialog .login-fields input[type="password"]:focus { 
    background-color: #ffffff !important; 
    border-color: #673de6 !important; 
    box-shadow: 0 0 10px rgba(103, 61, 230, 0.15) !important; 
    outline: none !important; 
}

.admin-notice {
    font-size: 11px !important;
    color: #727586 !important;
    text-align: center !important;
    margin-top: 20px !important;
    font-weight: 500 !important;
    line-height: 1.5 !important;
}

/* Tombol Masuk di Login (Violet) */
.login-right-panel .buttons input[type="submit"] {
    background-image: none !important; 
    background: linear-gradient(135deg, #673de6, #5025d1) !important; 
    border: none !important; 
    color: white !important; 
    border-radius: 50px !important; 
    padding: 12px !important; 
    font-size: 14px !important; 
    font-weight: 800 !important; 
    text-transform: uppercase !important; 
    letter-spacing: 1.5px !important; 
    cursor: pointer !important; 
    box-shadow: 0 5px 15px rgba(103, 61, 230, 0.25) !important; 
    transition: all 0.3s ease !important; 
    width: 100% !important; 
}
.login-right-panel .buttons input[type="submit"]:hover { 
    background: linear-gradient(135deg, #8c85ff, #673de6) !important; 
    transform: translateY(-2px) !important; 
    box-shadow: 0 8px 20px rgba(103, 61, 230, 0.4) !important; 
}

/* Error Alert Box */
.login-ui .login-error {
    display: none !important;
}
.login-ui.error .login-error {
    display: block !important;
    background: #ffe8ef !important;
    border: 1px solid rgba(252, 81, 133, 0.3) !important;
    border-radius: 8px !important;
    color: #d63163 !important; 
    padding: 12px 20px !important;
    text-align: center !important;
    margin-bottom: 20px !important;
    font-weight: 600 !important;
    font-size: 14px !important;
    box-shadow: 0 4px 10px rgba(252, 81, 133, 0.08) !important;
    max-width: 780px !important;
    width: 95% !important;
    box-sizing: border-box !important;
}

/* RESPONSIVE MOBILE ADJUSTMENTS */
@media (max-width: 850px) {
    .login-ui {
        padding: 15px !important;
        flex-direction: column !important;
    }
    
    .login-dialog.custom-split-layout {
        flex-direction: column !important;
        width: 100% !important;
        max-width: 420px !important;
        height: auto !important;
        margin: 10px auto !important;
        transform: none !important;
        border-radius: 20px !important;
    }
    
    .login-left-panel {
        width: 100% !important;
        height: auto !important;
        padding: 1.8rem 1.5rem !important;
        border-top-left-radius: 20px !important;
        border-top-right-radius: 20px !important;
        border-bottom-right-radius: 0 !important;
        border-bottom-left-radius: 40px 15px !important;
        border-bottom-right-radius: 40px 15px !important;
        box-shadow: 0 5px 15px rgba(0,0,0,0.08) !important;
        justify-content: center !important;
        align-items: center !important;
    }
    
    .login-left-panel .logo {
        width: 75px !important;
        height: 85px !important;
        margin: 0 auto 10px auto !important;
        filter: drop-shadow(0 4px 8px rgba(0,0,0,0.2)) !important;
    }
    
    .login-left-panel .brand-title {
        font-size: 18px !important;
        margin-top: 5px !important;
        letter-spacing: 0.5px !important;
    }
    
    .login-left-panel .brand-subtitle {
        font-size: 11px !important;
        margin-top: 3px !important;
        letter-spacing: 1px !important;
    }
    
    .login-right-panel {
        width: 100% !important;
        padding: 2rem 1.8rem !important;
    }
    
    .login-title {
        font-size: 20px !important;
        margin-bottom: 20px !important;
    }
    
    .login-dialog .login-fields .username-field input,
    .login-dialog .login-fields .password-field input {
        padding: 11px 20px 11px 42px !important;
        font-size: 13px !important;
        margin-bottom: 10px !important;
        background-position: left 15px center !important;
        background-size: 16px !important;
    }
    
    .login-right-panel .buttons input[type="submit"] {
        padding: 11px !important;
        font-size: 13px !important;
    }
}

/* --- 3. THEMA DASHBOARD POST-LOGIN (ISOLATED IN #content) --- */
#content {
    background: #f2f3f6 !important;
    min-height: 100vh !important;
    box-sizing: border-box !important;
    font-family: 'Outfit', sans-serif !important;
    color: #1d1e20 !important;
}

/* Base Text elements inside content */
#content p,
#content label,
#content span:not(.button):not(.home) {
    color: #36344d !important;
}

/* Header Halaman Utama (Terang) */
#content .header, #content .header.tabbed {
    position: relative !important;
    z-index: 100 !important;
    background: #ffffff !important;
    border-bottom: 1px solid #dadce0 !important;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05) !important;
    color: #1d1e20 !important;
    padding: 15px 25px !important;
}

#content .header h2 {
    color: #1d1e20 !important;
    font-weight: 700 !important;
    letter-spacing: 0.5px !important;
    font-size: 1.35em !important;
}

/* Kotak Pencarian / Filter Group */
#content .header .filter input {
    background-color: #ffffff !important;
    border: 1px solid #dadce0 !important;
    color: #1d1e20 !important;
    border-radius: 20px !important;
    padding: 8px 18px !important;
    transition: all 0.3s ease !important;
    font-size: 0.85em !important;
}
#content .header .filter input:focus {
    border-color: #673de6 !important;
    box-shadow: 0 0 8px rgba(103, 61, 230, 0.15) !important;
    outline: none !important;
}

/* Container Utama Koneksi */
#content .all-connections, #content .recent-connections {
    background: #ffffff !important;
    border: 1px solid #dadce0 !important;
    border-radius: 16px !important;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04) !important;
    margin: 1.5rem !important;
    padding: 20px !important;
}

/* Link-Link di dalam Content */
#content a, 
#content a:visited {
    color: #673de6 !important; 
    text-decoration: none !important;
    transition: all 0.2s ease !important;
}
#content a:hover {
    color: #5025d1 !important;
    text-decoration: underline !important;
}

/* Item Grup Koneksi, User, dan Koneksi */
#content .connection, #content .connection-group, #content .user, #content .user-group {
    border-radius: 8px !important;
    transition: all 0.25s ease !important;
    border: 1px solid transparent !important;
}
#content .connection:hover, #content .connection-group:hover, #content .user:hover, #content .user-group:hover {
    background: rgba(103, 61, 230, 0.04) !important;
    border-color: rgba(103, 61, 230, 0.1) !important;
    transform: translateX(4px) !important;
}

/* Recent Connections */
#content div.recent-connections div.connection {
    background: #f2f3f6 !important;
    border: 1px solid #dadce0 !important;
    border-radius: 12px !important;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04) !important;
}
#content div.recent-connections div.connection:hover {
    background: rgba(103, 61, 230, 0.05) !important;
    border-color: #673de6 !important;
}

/* Menu Dropdown Pengguna (User Menu) */
.menu-dropdown,
.menu-dropdown:hover,
.menu-dropdown.open,
.menu-dropdown.open:hover,
.menu-dropdown:active,
.menu-dropdown:focus,
.user-menu .menu-dropdown,
.user-menu .menu-dropdown:hover,
.user-menu .menu-dropdown.open,
.user-menu .menu-dropdown.open:hover,
.user-menu .menu-dropdown:active,
.user-menu .menu-dropdown:focus {
    background: transparent !important;
    border: none !important;
    box-shadow: none !important;
}

.user-menu .menu-dropdown .menu-title {
    color: #1d1e20 !important;
    font-weight: 600 !important;
    background-color: rgba(29, 30, 32, 0.04) !important;
    border: 1px solid #dadce0 !important;
    border-radius: 20px !important;
    padding: 8px 25px !important; 
    background-image: none !important; 
    transition: all 0.3s ease !important;
}

.user-menu .menu-dropdown .menu-title:hover,
.user-menu .menu-dropdown.open .menu-title,
.user-menu .menu-dropdown .menu-title:active,
.user-menu .menu-dropdown .menu-title:focus {
    background-color: rgba(103, 61, 230, 0.08) !important;
    border-color: #673de6 !important;
    box-shadow: 0 0 8px rgba(103, 61, 230, 0.15) !important;
    outline: none !important;
}

.user-menu .menu-dropdown .menu-indicator {
    display: none !important;
}

.menu-dropdown .menu-contents {
    z-index: 1000 !important;
    background: #ffffff !important;
    border: 1px solid #dadce0 !important;
    border-radius: 12px !important;
    box-shadow: 0 10px 30px rgba(29, 30, 32, 0.08) !important;
    padding: 6px !important;
    min-width: 220px !important;
}

/* User Menu Profile Section */
.user-menu .menu-dropdown .menu-contents .profile {
    margin: 10px 15px 15px 15px !important;
    padding-bottom: 12px !important;
    border-bottom: 1px solid #dadce0 !important;
    color: #36344d !important;
}
.user-menu .menu-dropdown .menu-contents .profile .full-name {
    color: #1d1e20 !important;
    font-weight: 700 !important;
    font-size: 1.05em !important;
}
.user-menu .menu-dropdown .menu-contents .profile .organizational-role,
.user-menu .menu-dropdown .menu-contents .profile .organization {
    color: #727586 !important;
    font-weight: 500 !important;
    font-size: 0.85em !important;
    margin-top: 2px !important;
}

/* Tombol secara Global */
a.button, 
button, 
input[type=submit], 
input[type=button], 
.button {
    background-color: #673de6 !important;
    background: linear-gradient(135deg, #8c85ff, #5025d1) !important;
    border: 1px solid rgba(255, 255, 255, 0.15) !important;
    color: #ffffff !important;
    border-radius: 8px !important;
    padding: 8px 16px !important;
    font-weight: 700 !important;
    box-shadow: 0 4px 14px rgba(103, 61, 230, 0.2) !important;
    cursor: pointer !important;
    transition: all 0.2s ease !important;
}
#content a.button, 
#content button, 
#content input[type=submit], 
#content input[type=button], 
#content .button {
    color: #ffffff !important;
}
a.button:hover, 
button:hover, 
input[type=submit]:hover, 
input[type=button]:hover, 
.button:hover {
    background: linear-gradient(135deg, #a7a2ff, #673de6) !important;
    transform: translateY(-1px) !important;
    box-shadow: 0 6px 18px rgba(103, 61, 230, 0.3) !important;
}

/* Remove default action icons inside buttons for cleaner layout */
a.button::before, 
button::before, 
.button::before,
#content a.button::before,
#content button::before,
#content .button::before {
    display: none !important;
}
a.button, 
button, 
.button,
#content a.button,
#content button,
#content .button {
    padding-left: 16px !important; 
    padding-right: 16px !important; 
}

/* Danger/Delete buttons */
button.danger,
a.button.danger,
.danger {
    background: linear-gradient(135deg, #fc5185, #d63163) !important; 
    border: 1px solid rgba(255, 255, 255, 0.15) !important;
    box-shadow: 0 4px 14px rgba(252, 81, 133, 0.2) !important;
}
#content button.danger,
#content a.button.danger,
#content .danger {
    color: #ffffff !important;
}
button.danger:hover,
a.button.danger:hover,
.danger:hover {
    background: linear-gradient(135deg, #fea8c2, #fc5185) !important;
    box-shadow: 0 6px 18px rgba(252, 81, 133, 0.3) !important;
}

/* Settings tab navigation */
#content .page-tabs .page-list a {
    color: #1d1e20 !important;
    background-color: rgba(29, 30, 32, 0.04) !important;
    border: 1px solid #dadce0 !important;
    border-radius: 6px !important;
    padding: 8px 16px !important;
    margin: 4px !important;
    font-weight: 600 !important;
    display: inline-block !important;
    transition: all 0.2s ease !important;
}
#content .page-tabs .page-list a:hover {
    color: #673de6 !important;
    background-color: rgba(103, 61, 230, 0.05) !important;
    border-color: #673de6 !important; 
    text-decoration: none !important;
    box-shadow: 0 4px 12px rgba(103, 61, 230, 0.05) !important;
}
#content .page-tabs .page-list a.current {
    color: #ffffff !important;
    background: linear-gradient(135deg, #8c85ff, #5025d1) !important;
    border-color: #673de6 !important;
    box-shadow: 0 4px 14px rgba(103, 61, 230, 0.25) !important;
}

/* Settings Forms */
#content .settings {
    background: #ffffff !important;
    border: 1px solid #dadce0 !important;
    border-radius: 16px !important;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04) !important;
    padding: 30px !important;
    color: #1d1e20 !important;
}

/* Settings Tables */
#content table.sorted th,
#content table.fields th,
#content table th {
    background: rgba(103, 61, 230, 0.05) !important;
    color: #1d1e20 !important;
    border-color: #dadce0 !important;
    font-weight: 600 !important;
}
#content table.sorted td,
#content table.fields td,
#content table td {
    color: #36344d !important;
    border-color: #f2f3f6 !important;
}

/* Input Fields inside settings */
#content input[type=email], #content input[type=number], #content input[type=password], #content input[type=text], #content textarea, #content select {
    background-color: #ffffff !important;
    border: 1px solid #dadce0 !important;
    color: #1d1e20 !important;
    border-radius: 6px !important;
    padding: 8px 12px !important;
    transition: all 0.2s ease !important;
}
#content input[type=email]:focus, #content input[type=number]:focus, #content input[type=password]:focus, #content input[type=text]:focus, #content textarea:focus, #content select:focus {
    border-color: #673de6 !important; 
    box-shadow: 0 0 8px rgba(103, 61, 230, 0.15) !important;
    outline: none !important;
}

#content h1,
#content h2,
#content h3,
#content h4,
#content h5,
#content h6,
#content legend {
    color: #1d1e20 !important;
}

/* Warning & Info boxes */
#content .notice.read-only,
#content .notice {
    background: #ffe8ef !important;
    border: 1px solid rgba(252, 81, 133, 0.3) !important;
    border-radius: 8px !important;
    color: #d63163 !important; 
    padding: 15px !important;
    text-align: center !important;
}

/* Scrollbar styling */
::-webkit-scrollbar {
    width: 8px !important;
    height: 8px !important;
}
::-webkit-scrollbar-track {
    background: #f2f3f6 !important;
}
::-webkit-scrollbar-thumb {
    background: rgba(103, 61, 230, 0.25) !important;
    border-radius: 4px !important;
}
::-webkit-scrollbar-thumb:hover {
    background: rgba(103, 61, 230, 0.4) !important;
}
```

---

### **Step 6: Build & Deploy Extension JAR**
Pack the assets into `pemkab-branding.jar`, copy it to the Guacamole extensions directory, and change its owner to `tomcat`:
```bash
jar cvf /root/install/pemkab-branding.jar -C /root/install/ guac-manifest.json -C /root/install/ app.css -C /root/install/ patch-templates.js -C /root/install/ images/ -C /root/install/ translations/
cp /root/install/pemkab-branding.jar /etc/guacamole/extensions/pemkab-branding.jar
chown tomcat:tomcat /etc/guacamole/extensions/pemkab-branding.jar
```

---

### **Step 7: Configure Tomcat ROOT Context**
Stop the Tomcat service, delete the default ROOT web application, and redeploy `guacamole.war` as the `ROOT.war` application:
```bash
systemctl stop tomcat
rm -rf /opt/tomcat/webapps/ROOT /opt/tomcat/webapps/guacamole
mv /opt/tomcat/webapps/guacamole.war /opt/tomcat/webapps/ROOT.war
systemctl start tomcat
# Wait 10 seconds for the application to unpack
sleep 10
```

---

### **Step 8: Generate Custom Favicons & Overwrite Assets**
Ensure python packages `python3-pil` and `python3-numpy` are installed, and execute the following Python script to generate multi-resolution `favicon.ico` and other favicon images directly over Tomcat's physical webapp resources:
```bash
python3 -c "
from PIL import Image
logo_path = '/root/install/images/logo.png'
# Overwrite physical images inside ROOT app
img = Image.open(logo_path)
img.resize((64, 64), Image.Resampling.LANCZOS).save('/opt/tomcat/webapps/ROOT/images/logo-64.png')
img.resize((144, 144), Image.Resampling.LANCZOS).save('/opt/tomcat/webapps/ROOT/images/logo-144.png')
img.save('/opt/tomcat/webapps/ROOT/favicon.ico', sizes=[(16, 16), (32, 32), (48, 48)])
print('Favicon and images overwritten successfully')
"
chown -R tomcat:tomcat /opt/tomcat/webapps/ROOT/images/logo-64.png /opt/tomcat/webapps/ROOT/images/logo-144.png /opt/tomcat/webapps/ROOT/favicon.ico
```

---

### **Step 9: Restart and Verify**
Restart the Tomcat container to clear all static asset cache layers:
```bash
systemctl restart tomcat
# Check Tomcat logs to confirm that pemkab-branding loaded successfully
tail -n 60 /opt/tomcat/logs/catalina.out | grep -E "extension|pemkab"
```
The logs should confirm that `[pemkab-branding] "Pemkab Branding"` loaded successfully.
Access the application directly at `http://localhost:8080/`.

Langkah-Langkah Penggunaan (Salin & Sesuaikan):

  1. Salin Prompt Kustomisasi:
    Klik tombol "Salin Prompt" pada kotak hitam di atas untuk menyalin seluruh isi kode master prompt ke clipboard Anda.
  2. Buat File Baru di Server Baru Anda:
    Hubungkan ke terminal server baru Anda, buat direktori instalasi, kemudian buka editor teks baru dengan perintah:
    mkdir -p /root/install && nano /root/install/guacamole_prompt.md
    Tempel (*paste*) kode prompt yang telah Anda salin tadi ke dalam editor nano tersebut.
  3. Sesuaikan Variabel Konfigurasi (PENTING):
    Sebelum menyimpan berkas, gulir ke bagian paling atas pada editor nano Anda. Cari bagian Configuration Variables dan sesuaikan isinya dengan instansi/daerah baru Anda:
    1. **Application Name (Title)**: [Ubah "E-BMD GATEWAY" menjadi nama aplikasi Anda] 2. **Municipality Name (Subtitle)**: [Ubah "PEMKAB LOMBOK UTARA" menjadi nama pemda Anda] 3. **Logo Image**: (Variabel ini menunjuk ke logo.png yang diletakkan di folder /root/install/images/logo.png)
    Setelah disesuaikan, simpan berkas dengan menekan Ctrl+O lalu tekan Enter. Kemudian, keluar dari editor dengan menekan Ctrl+X.
  4. Siapkan File Logo PNG Resmi:
    Taruh file logo PNG resmi instansi/aplikasi baru Anda di direktori tujuan: /root/install/images/logo.png.
  5. Perintahkan Agy untuk Mengeksekusi:
    Buka chat/console agy di server baru Anda, lalu kirimkan perintah berikut agar agy menginstal Guacamole dasar dari awal sekaligus menerapkan kustomisasi dari file prompt tadi:
    "Tolong instal Apache Guacamole dari awal di server bersih ini lengkap dengan Database MariaDB, Tomcat, dan guacd. Setelah instalasi dasar selesai berjalan, tolong baca instruksi kustomisasi di berkas /root/install/guacamole_prompt.md dan jalankan kustomisasinya."

Dipublikasikan di catatanku.swbill.my.id - Panduan Administrasi Sistem Guacamole Modern.

Tidak ada komentar:

Posting Komentar