Switch to local-build tailwindcss instead CDN

This commit is contained in:
2025-11-18 00:10:37 +01:00
parent 59e3a5e74f
commit ced2d0f3e0
9 changed files with 220 additions and 7 deletions

5
.gitignore vendored
View File

@@ -29,3 +29,8 @@ fail2ban-ui-settings.json
_dev _dev
fail2ban-ui.db* fail2ban-ui.db*
.DS_Store .DS_Store
# Node.js / Tailwind CSS build
node_modules/
package-lock.json
.tailwind-build/

View File

@@ -42,6 +42,8 @@ COPY --from=builder /app/fail2ban-ui /app/fail2ban-ui
RUN chown fail2ban:0 /app/fail2ban-ui && chmod +x /app/fail2ban-ui RUN chown fail2ban:0 /app/fail2ban-ui && chmod +x /app/fail2ban-ui
COPY --from=builder /app/pkg/web/templates /app/templates COPY --from=builder /app/pkg/web/templates /app/templates
COPY --from=builder /app/internal/locales /app/locales COPY --from=builder /app/internal/locales /app/locales
# Copy static files (Tailwind CSS) if they exist
COPY --from=builder /app/pkg/web/static /app/static
# Set environment variables # Set environment variables
ENV CONTAINER=true ENV CONTAINER=true

View File

@@ -55,7 +55,7 @@ Developed by **[Swissmakers GmbH](https://swissmakers.ch)**.
**Mobile-Friendly & Responsive UI / Fast** **Mobile-Friendly & Responsive UI / Fast**
- Optimized for **mobile & desktop** - Optimized for **mobile & desktop**
- Powered by **Bootstrap 5** - Powered by **Tailwind CSS** (works offline when built locally)
- **Go-based backend** ensures minimal resource usage - **Go-based backend** ensures minimal resource usage
- Parallel execution for improved performance on remote connections - Parallel execution for improved performance on remote connections
@@ -91,10 +91,24 @@ To install and run directly on the system:
```bash ```bash
git clone https://github.com/swissmakers/fail2ban-ui.git /opt/fail2ban-ui git clone https://github.com/swissmakers/fail2ban-ui.git /opt/fail2ban-ui
cd /opt/fail2ban-ui cd /opt/fail2ban-ui
# Build Tailwind CSS for production (optional but recommended)
# This requires Node.js and npm to be installed
./build-tailwind.sh
# Build Go application
go build -o fail2ban-ui ./cmd/server/main.go go build -o fail2ban-ui ./cmd/server/main.go
... ...
``` ```
**📌 Note on Tailwind CSS:**
- For **production/offline use**, build Tailwind CSS using `./build-tailwind.sh` before building the application
- This creates a local CSS file at `pkg/web/static/tailwind.css` that works offline
- The build script uses **Tailwind CSS v3** (latest v3.x, matches CDN version)
- If the local file is not found, the application will automatically fall back to the CDN (requires internet connection)
- The build script requires **Node.js** and **npm** to be installed
- The script works for both fresh installations and existing development environments
--- ---
### **🔹 Method 2: Running as a Container** ### **🔹 Method 2: Running as a Container**

97
build-tailwind.sh Executable file
View File

@@ -0,0 +1,97 @@
#!/bin/bash
# Build script for Tailwind CSS v3
# This script builds Tailwind CSS for production use
# Always installs latest Tailwind CSS v3 (matches CDN version)
set -e
echo "Building Tailwind CSS v3 for Fail2ban-UI..."
# Check if Node.js and npm are installed
if ! command -v node &> /dev/null; then
echo "Error: Node.js is not installed. Please install Node.js first."
echo "Visit: https://nodejs.org/"
exit 1
fi
if ! command -v npm &> /dev/null; then
echo "Error: npm is not installed. Please install npm first."
exit 1
fi
# Create directories if they don't exist
mkdir -p pkg/web/static
mkdir -p .tailwind-build
# Initialize package.json if it doesn't exist
if [ ! -f package.json ]; then
echo "Initializing npm package..."
npm init -y --silent
fi
# Install latest Tailwind CSS v3
echo "Installing latest Tailwind CSS v3..."
npm install -D tailwindcss@^3 --silent
# Verify the CLI binary exists
if [ ! -f "node_modules/.bin/tailwindcss" ] && [ ! -f "node_modules/tailwindcss/lib/cli.js" ]; then
echo "❌ Error: Tailwind CSS CLI not found after installation."
exit 1
fi
# Show installed version
INSTALLED_VERSION=$(npm list tailwindcss 2>/dev/null | grep "tailwindcss@" | head -1 | awk '{print $2}' || echo "unknown")
echo "Installed: $INSTALLED_VERSION"
# Create tailwind.config.js if it doesn't exist
if [ ! -f tailwind.config.js ]; then
echo "Creating Tailwind CSS configuration..."
cat > tailwind.config.js << 'EOF'
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./pkg/web/templates/**/*.html",
],
theme: {
extend: {},
},
plugins: [],
}
EOF
fi
# Create input CSS file if it doesn't exist
if [ ! -f .tailwind-build/input.css ]; then
echo "Creating input CSS file..."
cat > .tailwind-build/input.css << 'EOF'
@tailwind base;
@tailwind components;
@tailwind utilities;
EOF
fi
# Build Tailwind CSS
echo "Building Tailwind CSS..."
# Try different methods to run the CLI
if [ -f "node_modules/.bin/tailwindcss" ]; then
node_modules/.bin/tailwindcss -i .tailwind-build/input.css -o pkg/web/static/tailwind.css --minify
elif [ -f "node_modules/tailwindcss/lib/cli.js" ]; then
node node_modules/tailwindcss/lib/cli.js -i .tailwind-build/input.css -o pkg/web/static/tailwind.css --minify
elif command -v npx &> /dev/null; then
npx --yes tailwindcss -i .tailwind-build/input.css -o pkg/web/static/tailwind.css --minify
else
echo "❌ Error: Could not find Tailwind CSS CLI"
exit 1
fi
# Verify output file was created and is not empty
if [ ! -f "pkg/web/static/tailwind.css" ] || [ ! -s "pkg/web/static/tailwind.css" ]; then
echo "❌ Error: Output file was not created or is empty"
exit 1
fi
echo "✅ Tailwind CSS v3 built successfully!"
echo "Output: pkg/web/static/tailwind.css"
echo ""
echo "The application will now use the local Tailwind CSS file instead of the CDN."

View File

@@ -64,10 +64,12 @@ func main() {
// In container, templates are assumed to be in /app/templates // In container, templates are assumed to be in /app/templates
router.LoadHTMLGlob("/app/templates/*") router.LoadHTMLGlob("/app/templates/*")
router.Static("/locales", "/app/locales") router.Static("/locales", "/app/locales")
router.Static("/static", "/app/static")
} else { } else {
// When running locally, load templates from pkg/web/templates // When running locally, load templates from pkg/web/templates
router.LoadHTMLGlob("pkg/web/templates/*") router.LoadHTMLGlob("pkg/web/templates/*")
router.Static("/locales", "./internal/locales") router.Static("/locales", "./internal/locales")
router.Static("/static", "./pkg/web/static")
} }
// Register all application routes, including the static files and templates. // Register all application routes, including the static files and templates.

23
package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "fail2ban-ui",
"version": "1.0.0",
"description": "🚀 **Fail2Ban-UI** is a Swiss-made **web-based management interface** for [Fail2Ban](https://www.fail2ban.org/). It provides an intuitive dashboard to **monitor, configure, and manage Fail2Ban** instances in real time, supporting both local and remote Fail2ban servers.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/swissmakers/fail2ban-ui.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/swissmakers/fail2ban-ui/issues"
},
"homepage": "https://github.com/swissmakers/fail2ban-ui#readme",
"devDependencies": {
"tailwindcss": "^3.4.18"
}
}

File diff suppressed because one or more lines are too long

View File

@@ -22,8 +22,14 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
<title data-i18n="page.title">Fail2ban UI Dashboard</title> <title data-i18n="page.title">Fail2ban UI Dashboard</title>
<!-- Tailwind CSS --> <!-- Tailwind CSS - Try local first, fallback to CDN for development -->
<script src="https://cdn.tailwindcss.com"></script> <link rel="stylesheet" href="/static/tailwind.css" onerror="
console.warn('Local Tailwind CSS not found, using CDN. For production, build Tailwind CSS. See README.md for instructions.');
var script = document.createElement('script');
script.src = 'https://cdn.tailwindcss.com';
document.head.appendChild(script);
this.onerror = null;
">
<!-- Font Awesome for icons --> <!-- Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Select2 CSS --> <!-- Select2 CSS -->
@@ -655,14 +661,14 @@
<!-- ******************************************************************* --> <!-- ******************************************************************* -->
<!-- Jail Config Modal --> <!-- Jail Config Modal -->
<div id="jailConfigModal" class="hidden fixed inset-0 overflow-y-auto" style="z-index: 60;"> <div id="jailConfigModal" class="hidden fixed inset-0 overflow-y-auto" style="z-index: 60;">
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> <div class="flex items-center justify-center min-h-screen pt-4 px-2 sm:px-4 pb-20 text-center">
<div class="fixed inset-0 transition-opacity" aria-hidden="true"> <div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div class="absolute inset-0 bg-gray-500 opacity-75"></div> <div class="absolute inset-0 bg-gray-500 opacity-75"></div>
</div> </div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span> <span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full"> <div class="relative inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all my-4 sm:my-8 align-middle w-full max-w-full" style="max-width: 90vw;">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start"> <div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full"> <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
@@ -670,7 +676,26 @@
<span data-i18n="modal.filter_config">Filter Config:</span> <span id="modalJailName"></span> <span data-i18n="modal.filter_config">Filter Config:</span> <span id="modalJailName"></span>
</h3> </h3>
<div class="mt-4"> <div class="mt-4">
<textarea id="jailConfigTextarea" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 h-96 font-mono text-sm"></textarea> <textarea id="jailConfigTextarea"
class="w-full border border-gray-700 rounded-md px-4 py-3 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 h-96 font-mono text-sm bg-gray-900 text-green-400 resize-none overflow-auto"
spellcheck="false"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
data-lpignore="true"
data-1p-ignore="true"
data-bwignore="true"
data-form-type="other"
data-extension-ignore="true"
data-icloud-keychain-ignore="true"
data-safari-autofill="false"
role="textbox"
aria-label="Filter configuration editor"
name="filter-config-editor"
inputmode="text"
style="caret-color: #4ade80; line-height: 1.5; tab-size: 2; width: 100%; min-width: 100%; max-width: 100%; box-sizing: border-box; -webkit-appearance: none; appearance: none;"
wrap="off"
onfocus="preventExtensionInterference(this);"></textarea>
</div> </div>
</div> </div>
</div> </div>
@@ -2512,11 +2537,39 @@
//* Filter-mod and config-mod actions : * //* Filter-mod and config-mod actions : *
//******************************************************************* //*******************************************************************
// Prevent browser extensions (like iCloud Passwords) from interfering
function preventExtensionInterference(element) {
if (!element) return;
try {
// Ensure control property exists to prevent "Cannot read properties of undefined" errors
if (!element.control) {
Object.defineProperty(element, 'control', {
value: {
type: element.type || 'textarea',
name: element.name || 'filter-config-editor',
form: null,
autocomplete: 'off'
},
writable: false,
enumerable: false,
configurable: true
});
}
// Prevent extensions from adding their own properties
Object.seal(element.control);
} catch (e) {
// Silently ignore errors
}
}
function openJailConfigModal(jailName) { function openJailConfigModal(jailName) {
currentJailForConfig = jailName; currentJailForConfig = jailName;
var textArea = document.getElementById('jailConfigTextarea'); var textArea = document.getElementById('jailConfigTextarea');
textArea.value = ''; textArea.value = '';
// Prevent browser extensions from interfering
preventExtensionInterference(textArea);
document.getElementById('modalJailName').textContent = jailName; document.getElementById('modalJailName').textContent = jailName;
showLoading(true); showLoading(true);
@@ -2531,7 +2584,13 @@
return; return;
} }
textArea.value = data.config; textArea.value = data.config;
// Prevent extension interference before opening modal
preventExtensionInterference(textArea);
openModal('jailConfigModal'); openModal('jailConfigModal');
// Call again after a short delay to ensure it's set after modal is visible
setTimeout(function() {
preventExtensionInterference(textArea);
}, 100);
}) })
.catch(function(err) { .catch(function(err) {
showToast("Error: " + err, 'error'); showToast("Error: " + err, 'error');

10
tailwind.config.js Normal file
View File

@@ -0,0 +1,10 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./pkg/web/templates/**/*.html",
],
theme: {
extend: {},
},
plugins: [],
}