Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 24ab43cceb | |||
| 898ad38303 | |||
| 16ef30de83 | |||
| a45397d003 | |||
| eec6fd4b3d | |||
| 7c3c2cc41e |
@@ -0,0 +1,46 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
types: [closed]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
if: github.event.pull_request.merged == true && startsWith(github.head_ref, 'v')
|
||||||
|
runs-on: build-htz-01
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Extract Version
|
||||||
|
id: get_version
|
||||||
|
run: echo "VERSION=${{ github.head_ref }}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Generate Changelog
|
||||||
|
id: changelog
|
||||||
|
uses: https://github.com/metcalfc/changelog-generator@v4.6.2
|
||||||
|
with:
|
||||||
|
myToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
run: |
|
||||||
|
VERSION=${{ steps.get_version.outputs.VERSION }}
|
||||||
|
docker build -t cr.jdbnet.co.uk/public/echolog:$VERSION \
|
||||||
|
-t cr.jdbnet.co.uk/public/echolog:latest \
|
||||||
|
--build-arg VERSION=$VERSION \
|
||||||
|
.
|
||||||
|
docker push cr.jdbnet.co.uk/public/echolog:$VERSION
|
||||||
|
docker push cr.jdbnet.co.uk/public/echolog:latest
|
||||||
|
|
||||||
|
- name: Create Gitea Release
|
||||||
|
uses: https://gitea.com/actions/gitea-release-action@v1
|
||||||
|
with:
|
||||||
|
tag_name: ${{ steps.get_version.outputs.VERSION }}
|
||||||
|
name: ${{ steps.get_version.outputs.VERSION }}
|
||||||
|
body: ${{ steps.changelog.outputs.changelog }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
name: Release Please
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release-please:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
release_created: ${{ steps.release.outputs.release_created }}
|
|
||||||
steps:
|
|
||||||
- uses: googleapis/release-please-action@v4
|
|
||||||
id: release
|
|
||||||
with:
|
|
||||||
manifest-file: .release-please-manifest.json
|
|
||||||
config-file: .release-please-config.json
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
if: ${{ steps.release.outputs.release_created }}
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
if: ${{ steps.release.outputs.release_created }}
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
if: ${{ steps.release.outputs.release_created }}
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Read version
|
|
||||||
if: ${{ steps.release.outputs.release_created }}
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
VERSION=$(cat VERSION)
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
|
||||||
if: ${{ steps.release.outputs.release_created }}
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
ghcr.io/jdb-net/echolog:${{ env.VERSION }}
|
|
||||||
ghcr.io/jdb-net/echolog:latest
|
|
||||||
build-args: |
|
|
||||||
VERSION=${{ env.VERSION }}
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
name: Deploy to Kubernetes
|
|
||||||
needs: release-please
|
|
||||||
if: ${{ needs.release-please.outputs.release_created }}
|
|
||||||
runs-on: [ k3s-lan-01 ]
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Apply manifests
|
|
||||||
run: |
|
|
||||||
sudo kubectl replace -f deployment.yml --grace-period=60 --force
|
|
||||||
@@ -2,3 +2,4 @@ tailwindcss
|
|||||||
static/output.css
|
static/output.css
|
||||||
.env
|
.env
|
||||||
__pycache__
|
__pycache__
|
||||||
|
echolog.db
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"packages": {
|
|
||||||
".": {
|
|
||||||
"release-type": "simple",
|
|
||||||
"version-file": "VERSION"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
".": "1.1.2"
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
## [1.1.2](https://github.com/JDB-NET/echolog/compare/v1.1.1...v1.1.2) (2025-12-14)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* :bug: docker image in readme ([049271b](https://github.com/JDB-NET/echolog/commit/049271baeeaba9266aeac4603dd9f9588fec4bfb))
|
|
||||||
|
|
||||||
## [1.1.1](https://github.com/JDB-NET/echolog/compare/v1.1.0...v1.1.1) (2025-11-02)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* :bug: image name ([7334fb0](https://github.com/JDB-NET/echolog/commit/7334fb0ec6d077ffa1166f1b1d93194635f9454e))
|
|
||||||
|
|
||||||
## [1.1.0](https://github.com/JDB-NET/echolog/compare/v1.0.0...v1.1.0) (2025-11-02)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* :sparkles: add release please ([d2c4e08](https://github.com/JDB-NET/echolog/commit/d2c4e08e397c9e7ab9c30b2c04b04804fb81ad16))
|
|
||||||
* Added favicon and PWA ([1df4920](https://github.com/JDB-NET/echolog/commit/1df49204065529b07f4f6ba6ae8dfd9f74ba74d8))
|
|
||||||
* Added public build ([3187cbb](https://github.com/JDB-NET/echolog/commit/3187cbbcc50e57b6f459f7d5c116a49ad5b15f65))
|
|
||||||
* Added timezone environment variable ([7fb273d](https://github.com/JDB-NET/echolog/commit/7fb273d0abefbeeeaad7b81c244f48501b692f6a))
|
|
||||||
* Initial Commit ([853e78b](https://github.com/JDB-NET/echolog/commit/853e78b45f7c5d38d67d85468e35122250b1080c))
|
|
||||||
* streak counter to count how many days a user has posted consecutively ([200bc97](https://github.com/JDB-NET/echolog/commit/200bc973b8f410e9bba3ef2ace668a02ce33ce74))
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* environment variable names ([22481e5](https://github.com/JDB-NET/echolog/commit/22481e5af28581784ace8b76f666c5d5ece07c3b))
|
|
||||||
* Fixed search ([4c6328c](https://github.com/JDB-NET/echolog/commit/4c6328c2e90a368db07de5a2babf8fd3dac1808b))
|
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
FROM python:3.13-slim
|
FROM python:3.13-slim
|
||||||
|
LABEL org.opencontainers.image.vendor="JDB-NET"
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . /app
|
COPY . /app
|
||||||
|
ARG VERSION=unknown
|
||||||
|
ENV VERSION=${VERSION}
|
||||||
RUN pip install -r requirements.txt \
|
RUN pip install -r requirements.txt \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get install curl -y \
|
&& apt-get install curl -y \
|
||||||
|
|||||||
@@ -11,65 +11,45 @@ A Flask-based web application for personal homelab journaling. Track your daily
|
|||||||
- **Daily Journal Entries**: Create and manage journal entries for any date
|
- **Daily Journal Entries**: Create and manage journal entries for any date
|
||||||
- **Streak Tracking**: Automatically calculates consecutive days with entries
|
- **Streak Tracking**: Automatically calculates consecutive days with entries
|
||||||
- **Search Functionality**: Search by keyword or filter by specific date
|
- **Search Functionality**: Search by keyword or filter by specific date
|
||||||
- **PWA Support**: Progressive Web App with offline capabilities and installable on mobile devices
|
- **PWA Support**: Progressive Web App making this installable on mobile devices
|
||||||
- **Modern UI**: Beautiful gradient design with Tailwind CSS and responsive layout
|
- **Modern UI**: Beautiful gradient design with Tailwind CSS and responsive layout
|
||||||
- **Optional Authentication**: Enable login protection with environment variables
|
- **Optional Authentication**: Enable login protection with environment variables
|
||||||
- **MySQL Backend**: Robust data persistence with MySQL/MariaDB
|
- **Multiple Database Support**: Use MySQL/MariaDB or SQLite for data persistence
|
||||||
- **Docker Ready**: Easy deployment with Docker and Docker Compose
|
- **Docker Ready**: Easy deployment with Docker and Docker Compose
|
||||||
- **Kubernetes Support**: Includes Kubernetes deployment manifest
|
- **Kubernetes Support**: Includes Kubernetes deployment manifest
|
||||||
|
|
||||||
## Quick Start with Docker
|
## Quick Start with Docker
|
||||||
|
|
||||||
### Docker Run
|
For deployment examples, see the [examples folder](examples/).
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d \
|
|
||||||
--name echolog \
|
|
||||||
-p 5000:5000 \
|
|
||||||
-e MYSQL_HOST=10.10.2.27 \
|
|
||||||
-e MYSQL_USER=echolog \
|
|
||||||
-e MYSQL_PASSWORD=your_password \
|
|
||||||
-e MYSQL_DATABASE=echolog \
|
|
||||||
-e SECRET_KEY=your_secret_key \
|
|
||||||
-e TZ=Europe/London \
|
|
||||||
-e LOGIN_ENABLED=true \
|
|
||||||
-e LOGIN_USERNAME=admin \
|
|
||||||
-e LOGIN_PASSWORD=your_password \
|
|
||||||
ghcr.io/jdb-net/echolog:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker Compose
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
echolog:
|
|
||||||
image: ghcr.io/jdb-net/echolog:latest
|
|
||||||
container_name: echolog
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "5000:5000"
|
|
||||||
environment:
|
|
||||||
- MYSQL_HOST=10.10.2.27
|
|
||||||
- MYSQL_USER=echolog
|
|
||||||
- MYSQL_PASSWORD=your_password
|
|
||||||
- MYSQL_DATABASE=echolog
|
|
||||||
- SECRET_KEY=your_secret_key
|
|
||||||
- TZ=Europe/London
|
|
||||||
- LOGIN_ENABLED=true
|
|
||||||
- LOGIN_USERNAME=admin
|
|
||||||
- LOGIN_PASSWORD=your_password
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
|
### Database Selection
|
||||||
|
|
||||||
|
EchoLog supports both MySQL/MariaDB and SQLite:
|
||||||
|
|
||||||
|
- SQLite (default for quick start)
|
||||||
|
- MySQL/MariaDB
|
||||||
|
|
||||||
|
See the [examples folder](examples/) for docker-compose files for both database types.
|
||||||
|
|
||||||
### Environment Variables
|
### Environment Variables
|
||||||
|
|
||||||
|
#### Database Configuration
|
||||||
|
|
||||||
|
- `DB_TYPE`: Database type - `mysql` (default) or `sqlite`
|
||||||
|
|
||||||
|
**MySQL/MariaDB options:**
|
||||||
- `MYSQL_HOST`: MySQL/MariaDB host (default: localhost)
|
- `MYSQL_HOST`: MySQL/MariaDB host (default: localhost)
|
||||||
- `MYSQL_USER`: Database user (default: root)
|
- `MYSQL_USER`: Database user (default: root)
|
||||||
- `MYSQL_PASSWORD`: Database password (default: empty)
|
- `MYSQL_PASSWORD`: Database password (default: empty)
|
||||||
- `MYSQL_DATABASE`: Database name (default: echolog)
|
- `MYSQL_DATABASE`: Database name (default: echolog)
|
||||||
|
|
||||||
|
**SQLite options:**
|
||||||
|
- `SQLITE_DB`: Path to SQLite database file (default: echolog.db)
|
||||||
|
|
||||||
|
#### Application Configuration
|
||||||
|
|
||||||
- `SECRET_KEY`: Flask secret key for sessions (**REQUIRED in production!**)
|
- `SECRET_KEY`: Flask secret key for sessions (**REQUIRED in production!**)
|
||||||
- `TZ`: Timezone for date handling (default: UTC)
|
- `TZ`: Timezone for date handling (default: UTC)
|
||||||
- `LOGIN_ENABLED`: Enable login protection (default: false)
|
- `LOGIN_ENABLED`: Enable login protection (default: false)
|
||||||
@@ -78,6 +58,8 @@ services:
|
|||||||
|
|
||||||
### Database Setup
|
### Database Setup
|
||||||
|
|
||||||
|
#### MySQL/MariaDB
|
||||||
|
|
||||||
The application automatically initializes the database schema on first run. Ensure the database and user exist with appropriate permissions:
|
The application automatically initializes the database schema on first run. Ensure the database and user exist with appropriate permissions:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
@@ -87,6 +69,10 @@ GRANT ALL PRIVILEGES ON echolog.* TO 'echolog'@'%';
|
|||||||
FLUSH PRIVILEGES;
|
FLUSH PRIVILEGES;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### SQLite
|
||||||
|
|
||||||
|
SQLite requires no setup - the database file is created automatically on first run. Ensure the directory where the database file is stored is writable by the application.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Creating Entries
|
### Creating Entries
|
||||||
@@ -112,76 +98,6 @@ FLUSH PRIVILEGES;
|
|||||||
|
|
||||||
The streak counter automatically tracks consecutive days with journal entries. The streak includes today or yesterday to start, and continues as long as you have entries on consecutive days.
|
The streak counter automatically tracks consecutive days with journal entries. The streak includes today or yesterday to start, and continues as long as you have entries on consecutive days.
|
||||||
|
|
||||||
## Kubernetes Deployment
|
|
||||||
|
|
||||||
The project includes a Kubernetes deployment manifest. See `deployment.yml` for details.
|
|
||||||
|
|
||||||
**Example Kubernetes deployment:**
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: echolog
|
|
||||||
namespace: echolog
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
template:
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: echolog
|
|
||||||
image: ghcr.io/jdb-net/echolog:latest
|
|
||||||
ports:
|
|
||||||
- containerPort: 5000
|
|
||||||
env:
|
|
||||||
- name: SECRET_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: echolog-secrets
|
|
||||||
key: secret-key
|
|
||||||
- name: MYSQL_HOST
|
|
||||||
value: "mysql-service"
|
|
||||||
- name: MYSQL_USER
|
|
||||||
value: "echolog"
|
|
||||||
- name: MYSQL_PASSWORD
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: echolog-secrets
|
|
||||||
key: mysql-password
|
|
||||||
- name: MYSQL_DATABASE
|
|
||||||
value: "echolog"
|
|
||||||
- name: TZ
|
|
||||||
value: "Europe/London"
|
|
||||||
- name: LOGIN_ENABLED
|
|
||||||
value: "true"
|
|
||||||
- name: LOGIN_USERNAME
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: echolog-secrets
|
|
||||||
key: username
|
|
||||||
- name: LOGIN_PASSWORD
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: echolog-secrets
|
|
||||||
key: password
|
|
||||||
```
|
|
||||||
|
|
||||||
## Progressive Web App (PWA)
|
|
||||||
|
|
||||||
EchoLog can be installed as a Progressive Web App on supported devices:
|
|
||||||
|
|
||||||
- **Mobile**: Add to home screen from browser menu
|
|
||||||
- **Desktop**: Install from browser address bar
|
|
||||||
- **Offline**: Works offline with cached content
|
|
||||||
|
|
||||||
## Security Notes
|
|
||||||
|
|
||||||
- **ENABLE LOGIN** in production by setting `LOGIN_ENABLED=true`
|
|
||||||
- **CHANGE THE DEFAULT CREDENTIALS** if using authentication
|
|
||||||
- **CHANGE THE SECRET_KEY** in production - use a strong random string (e.g., `openssl rand -hex 32`)
|
|
||||||
- Use strong passwords for database access
|
|
||||||
- Ensure database connections are secured (consider SSL/TLS for MySQL connections)
|
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Database Connection Issues
|
### Database Connection Issues
|
||||||
|
|||||||
@@ -1,43 +1,60 @@
|
|||||||
import os
|
import os
|
||||||
import pytz
|
import pytz
|
||||||
import logging
|
import logging
|
||||||
|
import sqlite3
|
||||||
from flask import Flask, render_template, request, redirect, url_for, session, jsonify
|
from flask import Flask, render_template, request, redirect, url_for, session, jsonify
|
||||||
from datetime import datetime, date as datedate, datetime as dt
|
from datetime import datetime, date as datedate, datetime as dt
|
||||||
import mysql.connector
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
# Read version from VERSION file
|
load_dotenv()
|
||||||
try:
|
|
||||||
with open('VERSION', 'r') as f:
|
__version__ = os.getenv('VERSION', 'unknown')
|
||||||
__version__ = f.read().strip()
|
|
||||||
except FileNotFoundError:
|
|
||||||
__version__ = 'unknown'
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
TIMEZONE = os.getenv('TZ', 'UTC')
|
TIMEZONE = os.getenv('TZ', 'UTC')
|
||||||
tz = pytz.timezone(TIMEZONE)
|
tz = pytz.timezone(TIMEZONE)
|
||||||
|
|
||||||
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'defaultsecret')
|
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'defaultsecret')
|
||||||
app.config['LOGIN_ENABLED'] = os.getenv('LOGIN_ENABLED', 'false').lower() == 'true'
|
app.config['LOGIN_ENABLED'] = os.getenv('LOGIN_ENABLED', 'false').lower() == 'true'
|
||||||
|
|
||||||
|
# Database configuration
|
||||||
|
DB_TYPE = os.getenv('DB_TYPE', 'mysql').lower()
|
||||||
|
|
||||||
|
if DB_TYPE == 'mysql':
|
||||||
|
import mysql.connector
|
||||||
mysql_config = {
|
mysql_config = {
|
||||||
'user': os.getenv('MYSQL_USER', 'root'),
|
'user': os.getenv('MYSQL_USER', 'root'),
|
||||||
'password': os.getenv('MYSQL_PASSWORD', ''),
|
'password': os.getenv('MYSQL_PASSWORD', ''),
|
||||||
'host': os.getenv('MYSQL_HOST', 'localhost'),
|
'host': os.getenv('MYSQL_HOST', 'localhost'),
|
||||||
'database': os.getenv('MYSQL_DATABASE', 'echolog')
|
'database': os.getenv('MYSQL_DATABASE', 'echolog')
|
||||||
}
|
}
|
||||||
|
elif DB_TYPE == 'sqlite':
|
||||||
|
SQLITE_DB = os.getenv('SQLITE_DB', 'echolog.db')
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported DB_TYPE: {DB_TYPE}. Use 'sqlite' or 'mysql'")
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO, format='[%(asctime)s] %(levelname)s in %(module)s: %(message)s')
|
logging.basicConfig(level=logging.INFO, format='[%(asctime)s] %(levelname)s in %(module)s: %(message)s')
|
||||||
|
|
||||||
def get_db_connection():
|
def get_db_connection():
|
||||||
|
if DB_TYPE == 'mysql':
|
||||||
return mysql.connector.connect(**mysql_config)
|
return mysql.connector.connect(**mysql_config)
|
||||||
|
elif DB_TYPE == 'sqlite':
|
||||||
|
conn = sqlite3.connect(SQLITE_DB)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def dict_from_row(row):
|
||||||
|
"""Convert database row to dictionary"""
|
||||||
|
if DB_TYPE == 'mysql':
|
||||||
|
return row
|
||||||
|
elif DB_TYPE == 'sqlite':
|
||||||
|
return dict(row) if row else None
|
||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
if DB_TYPE == 'mysql':
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS journal_entry (
|
CREATE TABLE IF NOT EXISTS journal_entry (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
@@ -45,6 +62,14 @@ def init_db():
|
|||||||
content TEXT NOT NULL
|
content TEXT NOT NULL
|
||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
|
elif DB_TYPE == 'sqlite':
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS journal_entry (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
date DATE NOT NULL,
|
||||||
|
content TEXT NOT NULL
|
||||||
|
);
|
||||||
|
""")
|
||||||
conn.commit()
|
conn.commit()
|
||||||
cursor.close()
|
cursor.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -53,6 +78,9 @@ def calculate_streak():
|
|||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("SELECT date FROM journal_entry ORDER BY date DESC")
|
cursor.execute("SELECT date FROM journal_entry ORDER BY date DESC")
|
||||||
|
if DB_TYPE == 'mysql':
|
||||||
|
dates = [row[0] for row in cursor.fetchall()]
|
||||||
|
elif DB_TYPE == 'sqlite':
|
||||||
dates = [row[0] for row in cursor.fetchall()]
|
dates = [row[0] for row in cursor.fetchall()]
|
||||||
cursor.close()
|
cursor.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -89,14 +117,25 @@ def index():
|
|||||||
per_page = 7
|
per_page = 7
|
||||||
offset = (page - 1) * per_page
|
offset = (page - 1) * per_page
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
if DB_TYPE == 'mysql':
|
||||||
cursor.execute("SELECT SQL_CALC_FOUND_ROWS * FROM journal_entry ORDER BY date DESC LIMIT %s OFFSET %s", (per_page, offset))
|
cursor.execute("SELECT SQL_CALC_FOUND_ROWS * FROM journal_entry ORDER BY date DESC LIMIT %s OFFSET %s", (per_page, offset))
|
||||||
entries = cursor.fetchall()
|
entries = cursor.fetchall()
|
||||||
cursor.execute("SELECT FOUND_ROWS() as total")
|
cursor.execute("SELECT FOUND_ROWS() as total")
|
||||||
total = cursor.fetchone()['total']
|
total = cursor.fetchone()['total']
|
||||||
|
elif DB_TYPE == 'sqlite':
|
||||||
|
cursor.execute("SELECT COUNT(*) as total FROM journal_entry")
|
||||||
|
total = cursor.fetchone()[0]
|
||||||
|
cursor.execute("SELECT * FROM journal_entry ORDER BY date DESC LIMIT ? OFFSET ?", (per_page, offset))
|
||||||
|
entries = [dict_from_row(row) for row in cursor.fetchall()]
|
||||||
|
|
||||||
today = datetime.now(tz).date().isoformat()
|
today = datetime.now(tz).date().isoformat()
|
||||||
cursor.execute("SELECT * FROM journal_entry WHERE date = %s", (today,))
|
cursor.execute("SELECT * FROM journal_entry WHERE date = ?" if DB_TYPE == 'sqlite' else "SELECT * FROM journal_entry WHERE date = %s", (today,))
|
||||||
todays_entry = cursor.fetchone()
|
todays_entry = cursor.fetchone()
|
||||||
|
if DB_TYPE == 'sqlite' and todays_entry:
|
||||||
|
todays_entry = dict_from_row(todays_entry)
|
||||||
|
|
||||||
cursor.close()
|
cursor.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
has_prev = page > 1
|
has_prev = page > 1
|
||||||
@@ -112,12 +151,13 @@ def add_entry():
|
|||||||
if content.strip():
|
if content.strip():
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("SELECT id FROM journal_entry WHERE date = %s", (date,))
|
param_marker = "?" if DB_TYPE == 'sqlite' else "%s"
|
||||||
|
cursor.execute(f"SELECT id FROM journal_entry WHERE date = {param_marker}", (date,))
|
||||||
existing = cursor.fetchone()
|
existing = cursor.fetchone()
|
||||||
if existing:
|
if existing:
|
||||||
cursor.execute("UPDATE journal_entry SET content = %s WHERE date = %s", (content, date))
|
cursor.execute(f"UPDATE journal_entry SET content = {param_marker} WHERE date = {param_marker}", (content, date))
|
||||||
else:
|
else:
|
||||||
cursor.execute("INSERT INTO journal_entry (date, content) VALUES (%s, %s)", (date, content))
|
cursor.execute(f"INSERT INTO journal_entry (date, content) VALUES ({param_marker}, {param_marker})", (date, content))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
cursor.close()
|
cursor.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -129,19 +169,25 @@ def entry_for_date():
|
|||||||
if not date:
|
if not date:
|
||||||
return jsonify({'content': ''})
|
return jsonify({'content': ''})
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor()
|
||||||
cursor.execute("SELECT content FROM journal_entry WHERE date = %s", (date,))
|
param_marker = "?" if DB_TYPE == 'sqlite' else "%s"
|
||||||
|
cursor.execute(f"SELECT content FROM journal_entry WHERE date = {param_marker}", (date,))
|
||||||
entry = cursor.fetchone()
|
entry = cursor.fetchone()
|
||||||
cursor.close()
|
cursor.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({'content': entry['content'] if entry else ''})
|
if DB_TYPE == 'sqlite':
|
||||||
|
content = entry[0] if entry else ''
|
||||||
|
else:
|
||||||
|
content = entry['content'] if entry else ''
|
||||||
|
return jsonify({'content': content})
|
||||||
|
|
||||||
@app.route('/search', methods=['GET'])
|
@app.route('/search', methods=['GET'])
|
||||||
def search():
|
def search():
|
||||||
query = request.args.get('query', '')
|
query = request.args.get('query', '')
|
||||||
date = request.args.get('date', None)
|
date = request.args.get('date', None)
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor()
|
||||||
|
if DB_TYPE == 'mysql':
|
||||||
sql = "SELECT * FROM journal_entry WHERE 1=1"
|
sql = "SELECT * FROM journal_entry WHERE 1=1"
|
||||||
params = []
|
params = []
|
||||||
if query:
|
if query:
|
||||||
@@ -153,6 +199,19 @@ def search():
|
|||||||
sql += " ORDER BY date DESC"
|
sql += " ORDER BY date DESC"
|
||||||
cursor.execute(sql, params)
|
cursor.execute(sql, params)
|
||||||
entries = cursor.fetchall()
|
entries = cursor.fetchall()
|
||||||
|
elif DB_TYPE == 'sqlite':
|
||||||
|
sql = "SELECT * FROM journal_entry WHERE 1=1"
|
||||||
|
params = []
|
||||||
|
if query:
|
||||||
|
sql += " AND content LIKE ?"
|
||||||
|
params.append(f"%{query}%")
|
||||||
|
if date:
|
||||||
|
sql += " AND date = ?"
|
||||||
|
params.append(date)
|
||||||
|
sql += " ORDER BY date DESC"
|
||||||
|
cursor.execute(sql, params)
|
||||||
|
entries = [dict_from_row(row) for row in cursor.fetchall()]
|
||||||
|
|
||||||
cursor.close()
|
cursor.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
today = datetime.now(tz).date().isoformat()
|
today = datetime.now(tz).date().isoformat()
|
||||||
@@ -191,22 +250,26 @@ def require_login():
|
|||||||
@app.route('/edit/<int:entry_id>', methods=['GET', 'POST'])
|
@app.route('/edit/<int:entry_id>', methods=['GET', 'POST'])
|
||||||
def edit_entry(entry_id):
|
def edit_entry(entry_id):
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor()
|
||||||
|
param_marker = "?" if DB_TYPE == 'sqlite' else "%s"
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
date = request.form.get('date')
|
date = request.form.get('date')
|
||||||
content = request.form.get('content')
|
content = request.form.get('content')
|
||||||
cursor.execute("UPDATE journal_entry SET date=%s, content=%s WHERE id=%s", (date, content, entry_id))
|
cursor.execute(f"UPDATE journal_entry SET date={param_marker}, content={param_marker} WHERE id={param_marker}", (date, content, entry_id))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
cursor.close()
|
cursor.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
else:
|
else:
|
||||||
cursor.execute("SELECT * FROM journal_entry WHERE id=%s", (entry_id,))
|
cursor.execute(f"SELECT * FROM journal_entry WHERE id={param_marker}", (entry_id,))
|
||||||
entry = cursor.fetchone()
|
entry = cursor.fetchone()
|
||||||
cursor.close()
|
cursor.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
if not entry:
|
if not entry:
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
|
if DB_TYPE == 'sqlite':
|
||||||
|
entry = dict_from_row(entry)
|
||||||
return render_template('edit.html', entry=entry)
|
return render_template('edit.html', entry=entry)
|
||||||
|
|
||||||
@app.route('/edit_modal', methods=['POST'])
|
@app.route('/edit_modal', methods=['POST'])
|
||||||
@@ -217,7 +280,8 @@ def edit_entry_modal():
|
|||||||
if entry_id and date and content:
|
if entry_id and date and content:
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("UPDATE journal_entry SET date=%s, content=%s WHERE id=%s", (date, content, entry_id))
|
param_marker = "?" if DB_TYPE == 'sqlite' else "%s"
|
||||||
|
cursor.execute(f"UPDATE journal_entry SET date={param_marker}, content={param_marker} WHERE id={param_marker}", (date, content, entry_id))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
cursor.close()
|
cursor.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -227,7 +291,8 @@ def edit_entry_modal():
|
|||||||
def delete_entry(entry_id):
|
def delete_entry(entry_id):
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("DELETE FROM journal_entry WHERE id=%s", (entry_id,))
|
param_marker = "?" if DB_TYPE == 'sqlite' else "%s"
|
||||||
|
cursor.execute(f"DELETE FROM journal_entry WHERE id={param_marker}", (entry_id,))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
cursor.close()
|
cursor.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|||||||
+2
-2
@@ -15,14 +15,14 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: echolog
|
- name: echolog
|
||||||
image: ghcr.io/jdb-net/echolog:latest
|
image: cr.jdbnet.co.uk/public/echolog:latest
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 5000
|
- containerPort: 5000
|
||||||
name: "echolog"
|
name: "echolog"
|
||||||
env:
|
env:
|
||||||
- name: MYSQL_HOST
|
- name: MYSQL_HOST
|
||||||
value: "10.10.2.27"
|
value: "10.10.25.4"
|
||||||
- name: MYSQL_USER
|
- name: MYSQL_USER
|
||||||
value: "echolog"
|
value: "echolog"
|
||||||
- name: MYSQL_PASSWORD
|
- name: MYSQL_PASSWORD
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
mysql:
|
||||||
|
image: mariadb:latest
|
||||||
|
container_name: echolog-mysql
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- MYSQL_ROOT_PASSWORD=root_password
|
||||||
|
- MYSQL_DATABASE=echolog
|
||||||
|
- MYSQL_USER=echolog
|
||||||
|
- MYSQL_PASSWORD=echolog_password
|
||||||
|
volumes:
|
||||||
|
- mysql-data:/var/lib/mysql
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||||
|
timeout: 20s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
echolog:
|
||||||
|
image: cr.jdbnet.co.uk/public/echolog:latest
|
||||||
|
container_name: echolog
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
depends_on:
|
||||||
|
mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
- DB_TYPE=mysql
|
||||||
|
- MYSQL_HOST=mysql
|
||||||
|
- MYSQL_USER=echolog
|
||||||
|
- MYSQL_PASSWORD=echolog_password
|
||||||
|
- MYSQL_DATABASE=echolog
|
||||||
|
- SECRET_KEY=change-me-in-production
|
||||||
|
- TZ=Europe/London
|
||||||
|
- LOGIN_ENABLED=false
|
||||||
|
# Uncomment below to enable login protection
|
||||||
|
# - LOGIN_ENABLED=true
|
||||||
|
# - LOGIN_USERNAME=admin
|
||||||
|
# - LOGIN_PASSWORD=change-me
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mysql-data:
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
echolog:
|
||||||
|
image: cr.jdbnet.co.uk/public/echolog:latest
|
||||||
|
container_name: echolog
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
volumes:
|
||||||
|
- echolog-data:/data
|
||||||
|
environment:
|
||||||
|
- DB_TYPE=sqlite
|
||||||
|
- SQLITE_DB=/data/echolog.db
|
||||||
|
- SECRET_KEY=change-me-in-production
|
||||||
|
- TZ=Europe/London
|
||||||
|
- LOGIN_ENABLED=false
|
||||||
|
# Uncomment below to enable login protection
|
||||||
|
# - LOGIN_ENABLED=true
|
||||||
|
# - LOGIN_USERNAME=admin
|
||||||
|
# - LOGIN_PASSWORD=change-me
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
echolog-data:
|
||||||
+4
-1
@@ -1,5 +1,8 @@
|
|||||||
Flask
|
Flask
|
||||||
mysql-connector-python
|
|
||||||
python-dotenv
|
python-dotenv
|
||||||
pytz
|
pytz
|
||||||
gunicorn
|
gunicorn
|
||||||
|
# Optional database drivers:
|
||||||
|
# For MySQL/MariaDB support, uncomment the line below:
|
||||||
|
# mysql-connector-python
|
||||||
|
# SQLite is built-in with Python
|
||||||
|
|||||||
@@ -213,7 +213,7 @@
|
|||||||
<footer class="mt-8 py-6 border-t border-gray-800 text-center text-gray-500 text-sm">
|
<footer class="mt-8 py-6 border-t border-gray-800 text-center text-gray-500 text-sm">
|
||||||
<span class="mr-1"><a href="https://www.jdbnet.co.uk" target="_blank" rel="noopener" class="text-gray-500 hover:text-pink-400 hover:underline mx-1">JDB-NET</a> © {{ now.year }}</span>
|
<span class="mr-1"><a href="https://www.jdbnet.co.uk" target="_blank" rel="noopener" class="text-gray-500 hover:text-pink-400 hover:underline mx-1">JDB-NET</a> © {{ now.year }}</span>
|
||||||
·
|
·
|
||||||
<span class="mx-1"><a href="https://github.com/JDB-NET/echolog" target="_blank" rel="noopener" class="text-gray-500 hover:text-pink-400 hover:underline mx-1">v{{ version }}</a></span>
|
<span class="mx-1"><a href="https://git.jdbnet.co.uk/jamie/echolog" target="_blank" rel="noopener" class="text-gray-500 hover:text-pink-400 hover:underline mx-1">{{ version }}</a></span>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
Reference in New Issue
Block a user