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):
-
Salin Prompt Kustomisasi:
Klik tombol "Salin Prompt" pada kotak hitam di atas untuk menyalin seluruh isi kode master prompt ke clipboard Anda. -
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. -
Sesuaikan Variabel Konfigurasi (PENTING):
Sebelum menyimpan berkas, gulir ke bagian paling atas pada editor nano Anda. Cari bagianConfiguration Variablesdan sesuaikan isinya dengan instansi/daerah baru Anda:Setelah disesuaikan, simpan berkas dengan menekan Ctrl+O lalu tekan Enter. Kemudian, keluar dari editor dengan menekan Ctrl+X.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) -
Siapkan File Logo PNG Resmi:
Taruh file logo PNG resmi instansi/aplikasi baru Anda di direktori tujuan:/root/install/images/logo.png. -
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