feat: Initial Commit
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
FROM mcr.microsoft.com/devcontainers/python:3.13
|
||||||
|
|
||||||
|
WORKDIR /workspace
|
||||||
|
|
||||||
|
CMD ["sleep", "infinity"]
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "Flask Dev Container",
|
||||||
|
"build": {
|
||||||
|
"dockerfile": "Dockerfile"
|
||||||
|
},
|
||||||
|
"settings": {},
|
||||||
|
"extensions": ["ms-python.python"],
|
||||||
|
"postCreateCommand": "pip install -r requirements.txt; curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64; chmod +x tailwindcss-linux-x64; mv tailwindcss-linux-x64 tailwindcss",
|
||||||
|
"forwardPorts": [5000],
|
||||||
|
"remoteUser": "vscode"
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
deployment.yml
|
||||||
|
run.sh
|
||||||
|
README.md
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
tailwindcss
|
||||||
|
static/output.css
|
||||||
|
.env
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
stages:
|
||||||
|
- buildandpush
|
||||||
|
- deploy
|
||||||
|
|
||||||
|
variables:
|
||||||
|
IMAGE_NAME: "$HARBOR_REGISTRY/internal/echolog"
|
||||||
|
|
||||||
|
buildandpush:
|
||||||
|
stage: buildandpush
|
||||||
|
image: docker:latest
|
||||||
|
tags:
|
||||||
|
- docker
|
||||||
|
script:
|
||||||
|
- echo "$HARBOR_PASSWORD" | docker login $HARBOR_REGISTRY -u "$HARBOR_USERNAME" --password-stdin
|
||||||
|
- docker build -t $IMAGE_NAME:$IMAGE_TAG .
|
||||||
|
- docker tag $IMAGE_NAME:$IMAGE_TAG $IMAGE_NAME:latest
|
||||||
|
- docker push $IMAGE_NAME:$IMAGE_TAG
|
||||||
|
- docker push $IMAGE_NAME:latest
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
stage: deploy
|
||||||
|
tags:
|
||||||
|
- k3s-lan-01
|
||||||
|
script:
|
||||||
|
- sudo kubectl replace -f deployment.yml --grace-period=60 --force
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
FROM python:3.13-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . /app
|
||||||
|
RUN pip install -r requirements.txt \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install curl -y \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64 \
|
||||||
|
&& chmod +x tailwindcss-linux-x64 \
|
||||||
|
&& mv tailwindcss-linux-x64 tailwindcss \
|
||||||
|
&& ./tailwindcss -i ./static/input.css -o ./static/output.css --content "./templates/*.html" --minify \
|
||||||
|
&& rm tailwindcss
|
||||||
|
EXPOSE 5000
|
||||||
|
CMD ["python", "app.py"]
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import os
|
||||||
|
from flask import Flask, render_template, request, redirect, url_for, session
|
||||||
|
from datetime import datetime
|
||||||
|
from datetime import date as datedate
|
||||||
|
from datetime import datetime as dt
|
||||||
|
import mysql.connector
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'defaultsecret')
|
||||||
|
app.config['LOGIN_ENABLED'] = os.getenv('LOGIN_ENABLED', 'false').lower() == 'true'
|
||||||
|
|
||||||
|
mysql_config = {
|
||||||
|
'user': os.getenv('MYSQL_USER', 'root'),
|
||||||
|
'password': os.getenv('MYSQL_PASSWORD', ''),
|
||||||
|
'host': os.getenv('MYSQL_HOST', 'localhost'),
|
||||||
|
'database': os.getenv('MYSQL_DATABASE', 'echolog')
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_db_connection():
|
||||||
|
return mysql.connector.connect(**mysql_config)
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS journal_entry (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
date DATE NOT NULL,
|
||||||
|
content TEXT NOT NULL
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
page = request.args.get('page', 1, type=int)
|
||||||
|
per_page = 7
|
||||||
|
offset = (page - 1) * per_page
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
cursor.execute("SELECT SQL_CALC_FOUND_ROWS * FROM journal_entry ORDER BY date DESC LIMIT %s OFFSET %s", (per_page, offset))
|
||||||
|
entries = cursor.fetchall()
|
||||||
|
cursor.execute("SELECT FOUND_ROWS() as total")
|
||||||
|
total = cursor.fetchone()['total']
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
has_prev = page > 1
|
||||||
|
has_next = offset + per_page < total
|
||||||
|
today = datedate.today().isoformat()
|
||||||
|
now = dt.now()
|
||||||
|
return render_template('index.html', entries=entries, today=today, page=page, has_prev=has_prev, has_next=has_next, now=now)
|
||||||
|
|
||||||
|
@app.route('/add', methods=['POST'])
|
||||||
|
def add_entry():
|
||||||
|
date = request.form.get('date', datetime.today().strftime('%Y-%m-%d'))
|
||||||
|
content = request.form.get('content', '')
|
||||||
|
if content.strip():
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("INSERT INTO journal_entry (date, content) VALUES (%s, %s)", (date, content))
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
@app.route('/search', methods=['GET'])
|
||||||
|
def search():
|
||||||
|
query = request.args.get('query', '')
|
||||||
|
date = request.args.get('date', None)
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
sql = "SELECT * FROM journal_entry WHERE 1=1"
|
||||||
|
params = []
|
||||||
|
if query:
|
||||||
|
sql += " AND content LIKE %s"
|
||||||
|
params.append(f"%{query}%")
|
||||||
|
if date:
|
||||||
|
sql += " AND date = %s"
|
||||||
|
params.append(date)
|
||||||
|
sql += " ORDER BY date DESC"
|
||||||
|
cursor.execute(sql, params)
|
||||||
|
entries = cursor.fetchall()
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
return render_template('index.html', entries=entries)
|
||||||
|
|
||||||
|
@app.route('/login', methods=['GET', 'POST'])
|
||||||
|
def login():
|
||||||
|
if not app.config['LOGIN_ENABLED']:
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
username = request.form.get('username')
|
||||||
|
password = request.form.get('password')
|
||||||
|
if username == os.getenv('LOGIN_USERNAME', 'admin') and password == os.getenv('LOGIN_PASSWORD', 'admin'):
|
||||||
|
session['logged_in'] = True
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
else:
|
||||||
|
return render_template('login.html', error="Invalid credentials")
|
||||||
|
|
||||||
|
return render_template('login.html')
|
||||||
|
|
||||||
|
@app.route('/logout')
|
||||||
|
def logout():
|
||||||
|
session.pop('logged_in', None)
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def require_login():
|
||||||
|
if app.config['LOGIN_ENABLED'] and not session.get('logged_in') and request.endpoint not in ['login', 'static']:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
# Edit entry route
|
||||||
|
@app.route('/edit/<int:entry_id>', methods=['GET', 'POST'])
|
||||||
|
def edit_entry(entry_id):
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
if request.method == 'POST':
|
||||||
|
date = request.form.get('date')
|
||||||
|
content = request.form.get('content')
|
||||||
|
cursor.execute("UPDATE journal_entry SET date=%s, content=%s WHERE id=%s", (date, content, entry_id))
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
else:
|
||||||
|
cursor.execute("SELECT * FROM journal_entry WHERE id=%s", (entry_id,))
|
||||||
|
entry = cursor.fetchone()
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
if not entry:
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
return render_template('edit.html', entry=entry)
|
||||||
|
|
||||||
|
# Edit entry via modal
|
||||||
|
@app.route('/edit_modal', methods=['POST'])
|
||||||
|
def edit_entry_modal():
|
||||||
|
entry_id = request.form.get('id')
|
||||||
|
date = request.form.get('date')
|
||||||
|
content = request.form.get('content')
|
||||||
|
if entry_id and date and content:
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("UPDATE journal_entry SET date=%s, content=%s WHERE id=%s", (date, content, entry_id))
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
# Delete entry route
|
||||||
|
@app.route('/delete/<int:entry_id>', methods=['POST'])
|
||||||
|
def delete_entry(entry_id):
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("DELETE FROM journal_entry WHERE id=%s", (entry_id,))
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
init_db()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host='0.0.0.0', port=5000)
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: echolog
|
||||||
|
namespace: echolog
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: echolog
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: echolog
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: echolog
|
||||||
|
image: docker.jdbnet.co.uk/internal/echolog:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- containerPort: 5000
|
||||||
|
name: "echolog"
|
||||||
|
env:
|
||||||
|
- name: DB_HOST
|
||||||
|
value: "10.10.2.27"
|
||||||
|
- name: DB_USER
|
||||||
|
value: "echolog"
|
||||||
|
- name: DB_PASSWORD
|
||||||
|
value: "gHH0&nGWK!@8Y5"
|
||||||
|
- name: DB_NAME
|
||||||
|
value: "echolog"
|
||||||
|
- name: SECRET_KEY
|
||||||
|
value: "bgSNcrA0gZiRX9LbZmminf2LItEXeo"
|
||||||
|
- name: LOGIN_ENABLED
|
||||||
|
value: "true"
|
||||||
|
- name: LOGIN_USERNAME
|
||||||
|
value: "jamie"
|
||||||
|
- name: LOGIN_PASSWORD
|
||||||
|
value: "X2pl5g%AE5m$xr"
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: echolog-ingress-service
|
||||||
|
namespace: echolog
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: echolog
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 80
|
||||||
|
targetPort: 5000
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: echolog-ingress
|
||||||
|
namespace: echolog
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: echolog.jdb143.uk
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- pathType: Prefix
|
||||||
|
path: "/"
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: echolog-ingress-service
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
Flask
|
||||||
|
mysql-connector-python
|
||||||
|
python-dotenv
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "Generating CSS..."
|
||||||
|
./tailwindcss -i ./static/input.css -o ./static/output.css --content "./templates/*.html" --minify
|
||||||
|
|
||||||
|
echo "Starting app..."
|
||||||
|
python3 app.py
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 241 KiB |
@@ -0,0 +1 @@
|
|||||||
|
@import "tailwindcss"
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>EchoLog</title>
|
||||||
|
<link href="{{ url_for('static', filename='output.css') }}" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
|
||||||
|
</head>
|
||||||
|
<body class="bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 min-h-screen text-white">
|
||||||
|
<div class="container mx-auto max-w-2xl p-6">
|
||||||
|
<header class="mb-8 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<a href="/"><h1 class="text-4xl font-extrabold tracking-tight bg-gradient-to-r from-blue-400 via-purple-400 to-pink-400 bg-clip-text text-transparent">EchoLog</h1></a>
|
||||||
|
<p class="text-gray-400 mt-1">Your personal homelab journal</p>
|
||||||
|
</div>
|
||||||
|
{% if session.logged_in %}
|
||||||
|
<a href="/logout" class="text-gray-300 hover:text-pink-400 transition text-xl" title="Logout">
|
||||||
|
<i class="fas fa-sign-out-alt"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<form action="/add" method="POST" class="mb-8 bg-gray-800/80 rounded-xl shadow-lg p-6 border border-gray-700">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="date" class="block text-xs font-semibold text-gray-400 mb-1">Date</label>
|
||||||
|
<input type="date" id="date" name="date" class="w-full p-2 bg-gray-900 border border-gray-700 rounded focus:ring-2 focus:ring-blue-500" required value="{{ today }}">
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="content" class="block text-xs font-semibold text-gray-400 mb-1">Journal Entry</label>
|
||||||
|
<textarea id="content" name="content" rows="4" class="w-full p-3 bg-gray-900 border border-gray-700 rounded focus:ring-2 focus:ring-blue-500 resize-vertical" placeholder="What did you do today?" required></textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="w-full py-2 bg-gradient-to-r from-blue-500 to-pink-500 text-white font-bold rounded shadow hover:scale-105 transition">Add Entry</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form action="/search" method="GET" class="mb-8 flex flex-col gap-4 items-stretch md:flex-row md:items-end">
|
||||||
|
<div class="flex-1">
|
||||||
|
<label for="query" class="block text-xs font-semibold text-gray-400 mb-1">Search</label>
|
||||||
|
<input type="text" id="query" name="query" class="w-full p-2 bg-gray-900 border border-gray-700 rounded" placeholder="Search by keyword...">
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<label for="date_search" class="block text-xs font-semibold text-gray-400 mb-1">Date</label>
|
||||||
|
<input type="date" id="date_search" name="date" class="w-full p-2 bg-gray-900 border border-gray-700 rounded">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="px-6 py-2 bg-gradient-to-r from-purple-500 to-pink-500 text-white font-bold rounded shadow hover:scale-105 transition w-full md:w-auto">Filter</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 class="text-2xl font-bold mb-4 text-pink-400">Previous Entries</h2>
|
||||||
|
<ul class="space-y-6">
|
||||||
|
{% for entry in entries %}
|
||||||
|
<li class="p-5 bg-gray-800/80 rounded-xl border border-gray-700 shadow hover:shadow-lg transition">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span class="text-xs text-gray-400 font-mono">{{ entry.date }}</span>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button type="button" class="text-blue-400 hover:text-blue-200" title="Edit" onclick='openEditModal("{{ entry.id|escape }}", "{{ entry.date|escape }}", {{ entry.content|tojson|safe }})'><i class="fas fa-edit"></i></button>
|
||||||
|
<button type="button" class="text-pink-400 hover:text-pink-200" title="Delete" onclick='openDeleteModal("{{ entry.id|escape }}")'><i class="fas fa-trash-alt"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pre class="whitespace-pre-wrap break-words text-base text-gray-100 font-sans">{{ entry.content }}</pre>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="text-gray-500 text-center">No entries found.</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Edit Modal -->
|
||||||
|
<div id="edit-modal" class="fixed inset-0 bg-black/30 backdrop-blur-sm flex items-center justify-center z-50 hidden">
|
||||||
|
<div class="bg-gray-900 rounded-xl shadow-2xl p-8 w-full max-w-lg border border-gray-700 relative">
|
||||||
|
<button onclick="closeEditModal()" class="absolute top-4 right-4 text-gray-400 hover:text-pink-400 text-2xl"><i class="fas fa-times"></i></button>
|
||||||
|
<h2 class="text-2xl font-bold mb-6 text-pink-400">Edit Entry</h2>
|
||||||
|
<form id="edit-form" action="/edit_modal" method="POST" class="space-y-4">
|
||||||
|
<input type="hidden" id="edit-id" name="id">
|
||||||
|
<div>
|
||||||
|
<label for="edit-date" class="block text-xs font-semibold text-gray-400 mb-1">Date</label>
|
||||||
|
<input type="date" id="edit-date" name="date" class="w-full p-2 bg-gray-800 border border-gray-700 rounded focus:ring-2 focus:ring-blue-500" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="edit-content" class="block text-xs font-semibold text-gray-400 mb-1">Journal Entry</label>
|
||||||
|
<textarea id="edit-content" name="content" rows="4" class="w-full p-3 bg-gray-800 border border-gray-700 rounded focus:ring-2 focus:ring-blue-500 resize-vertical" required></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<button type="submit" class="flex-1 py-2 bg-gradient-to-r from-blue-500 to-pink-500 text-white font-bold rounded shadow hover:scale-105 transition">Save Changes</button>
|
||||||
|
<button type="button" onclick="closeEditModal()" class="flex-1 py-2 bg-gray-700 text-white font-bold rounded shadow hover:bg-gray-600 transition">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Modal -->
|
||||||
|
<div id="delete-modal" class="fixed inset-0 bg-black/30 backdrop-blur-sm flex items-center justify-center z-50 hidden">
|
||||||
|
<div class="bg-gray-900 rounded-xl shadow-2xl p-8 w-full max-w-md border border-gray-700 relative">
|
||||||
|
<button onclick="closeDeleteModal()" class="absolute top-4 right-4 text-gray-400 hover:text-pink-400 text-2xl"><i class="fas fa-times"></i></button>
|
||||||
|
<h2 class="text-2xl font-bold mb-6 text-pink-400">Delete Entry</h2>
|
||||||
|
<p class="mb-6 text-gray-300">Are you sure you want to delete this entry? This action cannot be undone.</p>
|
||||||
|
<form id="delete-form" method="POST">
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<button type="submit" class="flex-1 py-2 bg-gradient-to-r from-blue-500 to-pink-500 text-white font-bold rounded shadow hover:scale-105 transition">Delete</button>
|
||||||
|
<button type="button" onclick="closeDeleteModal()" class="flex-1 py-2 bg-gray-700 text-white font-bold rounded shadow hover:bg-gray-600 transition">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8 flex justify-between">
|
||||||
|
{% if has_prev %}
|
||||||
|
<a href="?page={{ page - 1 }}" class="px-4 py-2 bg-gray-700 rounded hover:bg-gray-600">Previous</a>
|
||||||
|
{% else %}
|
||||||
|
<span></span>
|
||||||
|
{% endif %}
|
||||||
|
{% if has_next %}
|
||||||
|
<a href="?page={{ page + 1 }}" class="px-4 py-2 bg-gray-700 rounded hover:bg-gray-600">Next</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="mt-16 py-6 border-t border-gray-800 text-center text-gray-500 text-sm">
|
||||||
|
<span>JDB-NET © {{ now.year }}</span>
|
||||||
|
·
|
||||||
|
<a href="https://echolog.jdbnet.co.uk" target="_blank" rel="noopener" class="text-gray-500 hover:text-pink-400 hover:underline ml-1">Docs</a>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function openEditModal(id, date, content) {
|
||||||
|
document.getElementById('edit-modal').classList.remove('hidden');
|
||||||
|
document.getElementById('edit-id').value = id;
|
||||||
|
document.getElementById('edit-date').value = date;
|
||||||
|
document.getElementById('edit-content').value = content;
|
||||||
|
}
|
||||||
|
function closeEditModal() {
|
||||||
|
document.getElementById('edit-modal').classList.add('hidden');
|
||||||
|
}
|
||||||
|
function openDeleteModal(id) {
|
||||||
|
document.getElementById('delete-modal').classList.remove('hidden');
|
||||||
|
var form = document.getElementById('delete-form');
|
||||||
|
form.action = '/delete/' + id;
|
||||||
|
}
|
||||||
|
function closeDeleteModal() {
|
||||||
|
document.getElementById('delete-modal').classList.add('hidden');
|
||||||
|
document.getElementById('delete-form').action = '';
|
||||||
|
}
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
closeEditModal();
|
||||||
|
closeDeleteModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>EchoLog</title>
|
||||||
|
<link href="{{ url_for('static', filename='output.css') }}" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body class="bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 min-h-screen text-white flex items-center justify-center">
|
||||||
|
<div class="w-full max-w-md p-8 bg-gray-900/90 rounded-2xl shadow-2xl border border-gray-800">
|
||||||
|
<header class="mb-8 text-center">
|
||||||
|
<h1 class="text-4xl font-extrabold tracking-tight bg-gradient-to-r from-blue-400 via-purple-400 to-pink-400 bg-clip-text text-transparent">EchoLog</h1>
|
||||||
|
<p class="text-gray-400 mt-2">Sign in to your homelab journal</p>
|
||||||
|
</header>
|
||||||
|
<form action="/login" method="POST" class="space-y-6">
|
||||||
|
{% if error %}
|
||||||
|
<p class="text-red-500 text-center font-semibold">{{ error }}</p>
|
||||||
|
{% endif %}
|
||||||
|
<div>
|
||||||
|
<label for="username" class="block text-xs font-semibold text-gray-400 mb-1">Username</label>
|
||||||
|
<input type="text" id="username" name="username" class="w-full p-3 bg-gray-800 border border-gray-700 rounded focus:ring-2 focus:ring-blue-500" placeholder="Username" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password" class="block text-xs font-semibold text-gray-400 mb-1">Password</label>
|
||||||
|
<input type="password" id="password" name="password" class="w-full p-3 bg-gray-800 border border-gray-700 rounded focus:ring-2 focus:ring-blue-500" placeholder="Password" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="w-full py-3 bg-gradient-to-r from-blue-500 to-pink-500 text-white font-bold rounded shadow hover:scale-105 transition">Login</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user