Adds Schedule Configuration UI & DB

This commit is contained in:
Michael Beck 2025-03-31 00:36:53 +02:00
parent 36170462fa
commit 2b45c67ddf
5 changed files with 278 additions and 8 deletions

View File

@ -1,6 +1,7 @@
from flask import Flask
from .config import Config
from .db import db
from .models import init_schedule_config
def create_app(test_config=None):
app = Flask(__name__)
@ -13,6 +14,7 @@ def create_app(test_config=None):
with app.app_context():
db.create_all()
init_schedule_config()
@app.context_processor
def inject_app_title():

View File

@ -1,6 +1,3 @@
from flask_sqlalchemy import SQLAlchemy, current_app, g
db = SQLAlchemy()
db_name = 'scipaperloader.db'
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()

View File

@ -16,4 +16,38 @@ class Paper(db.Model):
class ScheduleConfig(db.Model):
hour = db.Column(db.Integer, primary_key=True) # 0-23
volume = db.Column(db.Float) # weight or count
weight = db.Column(db.Float) # weight
class VolumeConfig(db.Model):
id = db.Column(db.Integer, primary_key=True)
volume = db.Column(db.Float) # volume of papers to scrape per day
def init_schedule_config():
"""Initialize ScheduleConfig with default values if empty"""
if ScheduleConfig.query.count() == 0:
# Default schedule: Lower volume during business hours,
# higher volume at night
default_schedule = [
# Night hours (higher volume)
*[(hour, 1.0) for hour in range(0, 6)],
# Morning hours (low volume)
*[(hour, 0.3) for hour in range(6, 9)],
# Business hours (very low volume)
*[(hour, 0.2) for hour in range(9, 17)],
# Evening hours (medium volume)
*[(hour, 0.5) for hour in range(17, 21)],
# Late evening (high volume)
*[(hour, 0.8) for hour in range(21, 24)]
]
for hour, weight in default_schedule:
config = ScheduleConfig(hour=hour, weight=weight)
db.session.add(config)
db.session.commit()
if VolumeConfig.query.count() == 0:
# Default volume configuration
default_volume = VolumeConfig(volume=100)
db.session.add(default_volume)
db.session.commit()

View File

@ -0,0 +1,222 @@
{% extends 'base.html' %}
{% block content %}
<style>
.timeline {
display: flex;
flex-wrap: wrap;
gap: 4px;
user-select: none; /* Prevent text selection during drag */
}
.hour-block {
width: 60px;
height: 70px; /* Increased height to fit additional text */
border-radius: 6px;
text-align: center;
line-height: 1.2;
font-size: 0.9rem;
padding-top: 6px;
cursor: pointer;
user-select: none;
transition: background-color 0.2s ease-in-out;
margin: 1px;
}
.hour-block.selected {
outline: 2px solid #4584b8;
}
.papers {
font-size: 0.7rem;
margin-top: 2px;
}
.flash-message {
padding: 12px;
margin-bottom: 20px;
border-radius: 6px;
opacity: 1;
transition: opacity 2s ease-in-out;
}
.flash-message.success {
background-color: #d4edda;
border-color: #c3e6cb;
color: #155724;
}
.flash-message.error {
background-color: #f8d7da;
border-color: #f5c6cb;
color: #721c24;
}
.flash-message.fade {
opacity: 0;
}
</style>
<script>
const initialSchedule = {{ schedule | tojson }};
const totalVolume = {{ volume }};
</script>
<div x-data="scheduleManager(initialSchedule, totalVolume)" class="container my-5">
<h2 class="mb-4">🕒 Configure Hourly Download Weights</h2>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div id="flash-messages">
{% for category, message in messages %}
<div class="flash-message {{ category }}" x-data="{}" x-init="setTimeout(() => $el.classList.add('fade'), 100); setTimeout(() => $el.remove(), 5000)">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<p class="text-muted">
Click to select one or more hours below. Then assign a weight to them using the input and apply it. Color indicates relative intensity. The total daily volume will be split proportionally across these weights.
</p>
<h3 class="mt-4">Volume</h3>
<p class="text-muted">The total volume of data to be downloaded each day is <strong x-text="volume"></strong> papers.</p>
<div class="d-flex justify-content-between align-items-start flex-wrap gap-2">
<p class="text-muted mb-0" style="max-width: 600px;">
Click to select one or more hours below. Then assign a weight to them using the input and apply it. Color indicates relative intensity. The total daily volume will be split proportionally across these weights.
</p>
<div class="hour-block"
style="pointer-events: none;"
x-init="$nextTick(() => previewWeight = 1.0)"
x-data="{ previewWeight: 1.0 }"
:style="getBackgroundStyleFromValue(previewWeight)">
<div><strong>14:00</strong></div>
<div class="weight"><span x-text="previewWeight.toFixed(1)"></span></div>
<div class="papers text-muted">example</div>
</div>
</div>
<h3 class="mt-4">Current Schedule</h3>
<form method="POST" action="{{ url_for('main.schedule') }}">
<div class="timeline mb-3" @mouseup="endDrag()" @mouseleave="endDrag()">
<template x-for="hour in Object.keys(schedule)" :key="hour">
<div class="hour-block"
:id="'hour-' + hour"
:data-hour="hour"
:style="getBackgroundStyle(hour)"
:class="{'selected': isSelected(hour)}"
@mousedown="startDrag($event, hour)"
@mouseover="dragSelect(hour)">
<div><strong x-text="formatHour(hour)"></strong></div>
<div class="weight"><span x-text="schedule[hour]"></span></div>
<div class="papers"><span x-text="getPapersPerHour(hour)"></span> p.</div>
<input type="hidden" :name="'hour_' + hour" :value="schedule[hour]">
</div>
</template>
</div>
<div class="input-group mb-4 w-50">
<label class="input-group-text">Set Weight:</label>
<input type="number" step="0.1" min="0" max="5" x-model="newWeight" class="form-control">
<button type="button" class="btn btn-outline-primary" @click="applyWeight()">Apply to Selected</button>
</div>
<div class="d-flex justify-content-between">
<a href="/" class="btn btn-outline-secondary">⬅ Back</a>
<button type="submit" class="btn btn-success">💾 Save Schedule</button>
</div>
</form>
</div>
<script>
function scheduleManager(initial, volume) {
return {
schedule: { ...initial },
volume: volume,
selectedHours: [],
newWeight: 1.0,
isDragging: false,
dragOperation: null,
formatHour(h) {
return String(h).padStart(2, '0') + ":00";
},
getBackgroundStyle(hour) {
const weight = parseFloat(this.schedule[hour]);
const maxWeight = 2.5; // You can adjust this
// Normalize weight (0.0 to 1.0)
const t = Math.min(weight / maxWeight, 1.0);
// Interpolate HSL lightness: 95% (light) to 30% (dark)
const lightness = 95 - (t * 65); // 95 → 30
const backgroundColor = `hsl(210, 10%, ${lightness}%)`; // soft gray-blue palette
const textColor = t > 0.65 ? 'white' : 'black'; // adaptive text color
return {
backgroundColor,
color: textColor
};
},
startDrag(event, hour) {
event.preventDefault();
this.isDragging = true;
this.dragOperation = this.isSelected(hour) ? 'remove' : 'add';
this.toggleSelect(hour);
},
dragSelect(hour) {
if (!this.isDragging) return;
const selected = this.isSelected(hour);
if (this.dragOperation === 'add' && !selected) {
this.selectedHours.push(hour);
} else if (this.dragOperation === 'remove' && selected) {
this.selectedHours = this.selectedHours.filter(h => h !== hour);
}
},
endDrag() {
this.isDragging = false;
},
toggleSelect(hour) {
if (this.isSelected(hour)) {
this.selectedHours = this.selectedHours.filter(h => h !== hour);
} else {
this.selectedHours.push(hour);
}
},
isSelected(hour) {
return this.selectedHours.includes(hour);
},
applyWeight() {
this.selectedHours.forEach(hour => {
this.schedule[hour] = parseFloat(this.newWeight).toFixed(1);
});
this.selectedHours = [];
},
getTotalWeight() {
return Object.values(this.schedule).reduce((sum, w) => sum + parseFloat(w), 0);
},
getPapersPerHour(hour) {
const total = this.getTotalWeight();
if (total === 0) return 0;
return ((parseFloat(this.schedule[hour]) / total) * this.volume).toFixed(1);
}
};
}
</script>{% endblock %}

View File

@ -1,4 +1,6 @@
from flask import Blueprint, render_template, current_app, request
from .models import ScheduleConfig, VolumeConfig
from .db import db
bp = Blueprint('main', __name__)
@ -17,9 +19,22 @@ def upload():
def papers():
return render_template("papers.html", app_title="PaperScraper")
@bp.route("/schedule")
@bp.route("/schedule", methods=["GET", "POST"])
def schedule():
return render_template("schedule.html", app_title="PaperScraper")
if request.method == "POST":
for hour in range(24):
key = f"hour_{hour}"
weight = float(request.form.get(key, 0))
config = ScheduleConfig.query.get(hour)
if config:
config.weight = weight
else:
db.session.add(ScheduleConfig(hour=hour, weight=weight))
db.session.commit()
schedule = {sc.hour: sc.weight for sc in ScheduleConfig.query.order_by(ScheduleConfig.hour).all()}
volume = VolumeConfig.query.first()
return render_template("schedule.html", schedule=schedule, volume=volume.volume, app_title="PaperScraper")
@bp.route("/logs")
def logs():