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
- name: Run type checking with ty
run: uv run ty check
run: uv run ty
- name: Run tests with pytest
run: uv run pytest

View File

@@ -1,6 +1,8 @@
name: Deploy
on:
push:
branches: ["main"]
workflow_run:
workflows: ["CI"]
types:
@@ -11,7 +13,7 @@ jobs:
deploy:
runs-on: ubuntu-latest
# 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:
- name: Checkout repository
@@ -45,7 +47,7 @@ jobs:
# Test if the application responds
sleep 10
wget --spider http://172.17.0.1:5123 || exit 1
curl -f http://localhost:5123 || exit 1
echo "Deployment successful!"

View File

@@ -1,6 +1,6 @@
# Caddyfile for HXBooks
# Replace 'localhost' with your domain for production with automatic HTTPS
:80 {
localhost {
# Serve static files directly (CSS, JS, images, etc.)
handle /static/* {
root * /var/www
@@ -33,6 +33,9 @@
# Forward real IP to app
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

View File

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

View File

@@ -12,9 +12,6 @@ else
echo "Static files already present in shared volume"
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
echo "Checking database status..."
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.
"""
import hashlib
import logging
from collections import defaultdict
from collections.abc import Sequence
@@ -1014,23 +1013,28 @@ def download_book_cover(book: Book, image_url: str) -> bool:
return False
# 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:
# Handle HTTP(S) URLs
response = requests.get(image_url, timeout=10, stream=True)
response.raise_for_status()
# Load image from response content
source = response.raw
with Image.open(source) as image:
with Image.open(response.raw) as image:
processed_image = _process_cover_image(image)
# Generate filename
extension = ".jpg" # Always save as JPEG
# Hash the image to create a unique filename based on content
image_hash = hashlib.md5(processed_image.tobytes()).hexdigest()
filename = f"book_{book.id}_{image_hash}{extension}"
filename = f"book_{book.id}{extension}"
cover_path = covers_dir / filename
# Save processed image

View File

@@ -303,21 +303,19 @@ def create_book() -> ResponseReturnValue:
except DuplicateISBNError as e:
flash(f"Error: {e}", "error")
return render_template("book/create.html.j2", form_data=request.form)
except ValidationError as e:
_flash_validation_errors(e)
return render_template("book/create.html.j2", form_data=request.form)
except Exception as e:
logger.error(f"Error creating book '{form_data.title}': {e}", exc_info=True)
flash(f"Error creating book: {e}", "error")
return render_template("book/create.html.j2", form_data=request.form)
return render_template(
"book/create.html.j2",
form_data=request.form,
genres=library.list_genres(),
authors=library.list_authors(),
locations=library.list_locations(),
)
return render_template("book/create.html.j2")
@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 %}
<div class="col-12 d-lg-none mb-3">
<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">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center gap-2">
@@ -110,7 +110,7 @@
<div class="card-header">
<h6 class="mb-0">{{ session.get('viewing_as_user').title() }}'s Data</h6>
</div>
<div class="card-body" id="user-status">
<div class="card-body">
{% include 'components/reading_status.html.j2' %}
{% include 'components/wishlist_status.html.j2' %}
</div>

View File

@@ -6,7 +6,7 @@
<h6 class="text-muted mb-2">📖 Reading Status</h6>
<!-- 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 %}
<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>
@@ -26,15 +26,15 @@
<!-- Current Book Rating (if any 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"
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 -->
<input type="hidden" name="start_date" value="{{ vars.completed_readings[0].start_date.strftime('%Y-%m-%d') }}">
<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 '' }}">
<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 '' }}">
<div class="col-auto">
@@ -61,8 +61,7 @@
{% 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 %}">
<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-target="this">
hx-swap="none show:none" hx-select-oob="#flash-messages-container:outerHTML" hx-target="this">
<div class="row">
<div class="col-md-6">
<label class="form-label-sm">Start Date</label>