Compare commits

..

1 Commits

Author SHA1 Message Date
8873728a4f Add CI and deployment workflows for Gitea
Some checks failed
Deploy / deploy (push) Failing after 59s
CI / quality-checks (push) Failing after 2m41s
2026-03-31 17:26:11 +02:00
9 changed files with 40 additions and 41 deletions

View File

@@ -29,7 +29,7 @@ jobs:
run: uv run pre-commit run --all-files run: uv run pre-commit run --all-files
- name: Run type checking with ty - name: Run type checking with ty
run: uv run ty check run: uv run ty
- name: Run tests with pytest - name: Run tests with pytest
run: uv run pytest run: uv run pytest

View File

@@ -1,6 +1,8 @@
name: Deploy name: Deploy
on: on:
push:
branches: ["main"]
workflow_run: workflow_run:
workflows: ["CI"] workflows: ["CI"]
types: types:
@@ -11,7 +13,7 @@ jobs:
deploy: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Only deploy if CI workflow succeeded # Only deploy if CI workflow succeeded
if: ${{ github.event.workflow_run.conclusion == 'success' }} if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'push' }}
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -45,7 +47,7 @@ jobs:
# Test if the application responds # Test if the application responds
sleep 10 sleep 10
wget --spider http://172.17.0.1:5123 || exit 1 curl -f http://localhost:5123 || exit 1
echo "Deployment successful!" echo "Deployment successful!"

View File

@@ -1,6 +1,6 @@
# Caddyfile for HXBooks # Caddyfile for HXBooks
# Replace 'localhost' with your domain for production with automatic HTTPS # Replace 'localhost' with your domain for production with automatic HTTPS
:80 { localhost {
# Serve static files directly (CSS, JS, images, etc.) # Serve static files directly (CSS, JS, images, etc.)
handle /static/* { handle /static/* {
root * /var/www root * /var/www
@@ -33,6 +33,9 @@
# Forward real IP to app # Forward real IP to app
header_up X-Real-IP {remote} header_up X-Real-IP {remote}
header_up X-Forwarded-For {remote}
header_up X-Forwarded-Proto {scheme}
header_up X-Forwarded-Host {host}
} }
# Optional: Enable compression for better performance # Optional: Enable compression for better performance

View File

@@ -9,8 +9,6 @@ services:
- instance:/app/instance - instance:/app/instance
# Mount shared directory for static files that Caddy can access # Mount shared directory for static files that Caddy can access
- static:/shared/static - static:/shared/static
# Mount caddy_file for Caddy configuration
- caddy_file:/app/caddy
expose: expose:
- "5000" - "5000"
environment: environment:
@@ -37,7 +35,7 @@ services:
- "5123:80" - "5123:80"
volumes: volumes:
# Caddyfile configuration # Caddyfile configuration
- caddy_file:/etc/caddy - ./Caddyfile:/etc/caddy/Caddyfile:ro
# Media files served directly by Caddy # Media files served directly by Caddy
- media:/var/www/media:ro - media:/var/www/media:ro
# Static files served directly by Caddy (populated by app container) # Static files served directly by Caddy (populated by app container)
@@ -66,5 +64,3 @@ volumes:
driver: local driver: local
caddy_config: caddy_config:
driver: local driver: local
caddy_file:
driver: local

View File

@@ -12,9 +12,6 @@ else
echo "Static files already present in shared volume" echo "Static files already present in shared volume"
fi fi
# Copy Caddyfile to shared volume
cp /app/Caddyfile /app/caddy/Caddyfile
# Initialize database if it doesn't exist or run migrations if it does # Initialize database if it doesn't exist or run migrations if it does
echo "Checking database status..." echo "Checking database status..."
if [ ! -f /app/instance/hxbooks.sqlite ]; then if [ ! -f /app/instance/hxbooks.sqlite ]; then

View File

@@ -5,7 +5,6 @@ Clean service layer for book management, reading tracking, and wishlist operatio
Separated from web interface concerns to enable both CLI and web access. Separated from web interface concerns to enable both CLI and web access.
""" """
import hashlib
import logging import logging
from collections import defaultdict from collections import defaultdict
from collections.abc import Sequence from collections.abc import Sequence
@@ -1014,23 +1013,28 @@ def download_book_cover(book: Book, image_url: str) -> bool:
return False return False
# Load image directly from file # Load image directly from file
source = source_path with Image.open(source_path) as image:
processed_image = _process_cover_image(image)
# Generate filename
extension = ".jpg" # Always save as JPEG
filename = f"book_{book.id}{extension}"
cover_path = covers_dir / filename
# Save processed image
processed_image.save(cover_path, "JPEG", quality=85)
else: else:
# Handle HTTP(S) URLs # Handle HTTP(S) URLs
response = requests.get(image_url, timeout=10, stream=True) response = requests.get(image_url, timeout=10, stream=True)
response.raise_for_status() response.raise_for_status()
# Load image from response content # Load image from response content
source = response.raw with Image.open(response.raw) as image:
with Image.open(source) as image:
processed_image = _process_cover_image(image) processed_image = _process_cover_image(image)
# Generate filename # Generate filename
extension = ".jpg" # Always save as JPEG extension = ".jpg" # Always save as JPEG
# Hash the image to create a unique filename based on content filename = f"book_{book.id}{extension}"
image_hash = hashlib.md5(processed_image.tobytes()).hexdigest()
filename = f"book_{book.id}_{image_hash}{extension}"
cover_path = covers_dir / filename cover_path = covers_dir / filename
# Save processed image # Save processed image

View File

@@ -303,21 +303,19 @@ def create_book() -> ResponseReturnValue:
except DuplicateISBNError as e: except DuplicateISBNError as e:
flash(f"Error: {e}", "error") flash(f"Error: {e}", "error")
return render_template("book/create.html.j2", form_data=request.form)
except ValidationError as e: except ValidationError as e:
_flash_validation_errors(e) _flash_validation_errors(e)
return render_template("book/create.html.j2", form_data=request.form)
except Exception as e: except Exception as e:
logger.error(f"Error creating book '{form_data.title}': {e}", exc_info=True) logger.error(f"Error creating book '{form_data.title}': {e}", exc_info=True)
flash(f"Error creating book: {e}", "error") flash(f"Error creating book: {e}", "error")
return render_template("book/create.html.j2", form_data=request.form)
return render_template( return render_template("book/create.html.j2")
"book/create.html.j2",
form_data=request.form,
genres=library.list_genres(),
authors=library.list_authors(),
locations=library.list_locations(),
)
@bp.route("/book/<int:book_id>/edit", methods=["POST"]) @bp.route("/book/<int:book_id>/edit", methods=["POST"])

View File

@@ -23,7 +23,7 @@
{% import 'components/user_book_vars.html.j2' as vars with context %} {% import 'components/user_book_vars.html.j2' as vars with context %}
<div class="col-12 d-lg-none mb-3"> <div class="col-12 d-lg-none mb-3">
<input type="checkbox" id="status-toggle" class="status-toggle-checkbox" hidden> <input type="checkbox" id="status-toggle" class="status-toggle-checkbox" hidden>
<div class="user-status-card" id="user-status-card"> <div class="user-status-card">
<label for="status-toggle" class="status-bar"> <label for="status-toggle" class="status-bar">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2">
@@ -110,7 +110,7 @@
<div class="card-header"> <div class="card-header">
<h6 class="mb-0">{{ session.get('viewing_as_user').title() }}'s Data</h6> <h6 class="mb-0">{{ session.get('viewing_as_user').title() }}'s Data</h6>
</div> </div>
<div class="card-body" id="user-status"> <div class="card-body">
{% include 'components/reading_status.html.j2' %} {% include 'components/reading_status.html.j2' %}
{% include 'components/wishlist_status.html.j2' %} {% include 'components/wishlist_status.html.j2' %}
</div> </div>

View File

@@ -6,7 +6,7 @@
<h6 class="text-muted mb-2">📖 Reading Status</h6> <h6 class="text-muted mb-2">📖 Reading Status</h6>
<!-- Action Buttons --> <!-- Action Buttons -->
<div class="mb-3" hx-select-oob="#flash-messages-container,#user-status-card,#user-status" hx-swap="none show:none"> <div class="mb-3">
{% if vars.current_reading %} {% if vars.current_reading %}
<form action="/book/{{ book.id }}/reading/finish" method="POST" class="d-inline"> <form action="/book/{{ book.id }}/reading/finish" method="POST" class="d-inline">
<button type="submit" class="btn btn-success btn-sm me-2">✓ Finish Reading</button> <button type="submit" class="btn btn-success btn-sm me-2">✓ Finish Reading</button>
@@ -26,15 +26,15 @@
<!-- Current Book Rating (if any completed readings) --> <!-- Current Book Rating (if any completed readings) -->
{% if vars.completed_readings %} {% if vars.completed_readings %}
<div id="book-rating" class="alert alert-light border py-2 mb-3"> <div class="alert alert-light border py-2 mb-3">
<form action="/book/{{ book.id }}/reading/{{ vars.completed_readings[0].id }}/update" method="POST" <form action="/book/{{ book.id }}/reading/{{ vars.completed_readings[0].id }}/update" method="POST"
class="row align-items-center g-2" hx-trigger="change,submit" hx-swap="none show:none" class="row align-items-center g-2" hx-trigger="change,submit" hx-swap="none show:none"
hx-select-oob="#flash-messages-container" hx-target="this"> hx-select-oob="#flash-messages-container:outerHTML" hx-target="this">
<!-- Hidden fields to preserve other reading data --> <!-- Hidden fields to preserve other reading data -->
<input type="hidden" name="start_date" value="{{ vars.completed_readings[0].start_date.strftime('%Y-%m-%d') }}"> <input type="hidden" name="start_date" value="{{ vars.completed_readings[0].start_date.strftime('%Y-%m-%d') }}">
<input type="hidden" name="end_date" <input type="hidden" name="end_date"
value="{{ vars.completed_readings[0].end_date.strftime('%Y-%m-%d') if vars.completed_readings[0].end_date else '' }}"> value="{{ vars.completed_readings[0].end_date.strftime('%Y-%m-%d') if vars.completed_readings[0].end_date else '' }}">
<input type="hidden" name="dropped" value="{{ '1' if vars.completed_readings[0].dropped else '0' }}"> <input type="hidden" name="dropped" value="1" {{ 'checked' if vars.completed_readings[0].dropped else '' }}>
<input type="hidden" name="comments" value="{{ vars.completed_readings[0].comments or '' }}"> <input type="hidden" name="comments" value="{{ vars.completed_readings[0].comments or '' }}">
<div class="col-auto"> <div class="col-auto">
@@ -61,8 +61,7 @@
{% for reading in vars.user_readings | sort(attribute='start_date', reverse=true) %} {% for reading in vars.user_readings | sort(attribute='start_date', reverse=true) %}
<div class="border rounded p-3 mb-2 {% if reading == vars.current_reading %}border-primary bg-light{% endif %}"> <div class="border rounded p-3 mb-2 {% if reading == vars.current_reading %}border-primary bg-light{% endif %}">
<form action="/book/{{ book.id }}/reading/{{ reading.id }}/update" method="POST" hx-trigger="change,submit" <form action="/book/{{ book.id }}/reading/{{ reading.id }}/update" method="POST" hx-trigger="change,submit"
hx-swap="none show:none" hx-select-oob="#flash-messages-container:outerHTML,#book-rating:outerHTML" hx-swap="none show:none" hx-select-oob="#flash-messages-container:outerHTML" hx-target="this">
hx-target="this">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label-sm">Start Date</label> <label class="form-label-sm">Start Date</label>