06/02 Optimize app 2
This commit is contained in:
@@ -217,3 +217,28 @@ def manual_snapshot():
|
|||||||
snap = take_net_worth_snapshot()
|
snap = take_net_worth_snapshot()
|
||||||
flash(f'Net worth snapshot saved: {snap.snapshot_date.strftime("%B %d, %Y")}', 'success')
|
flash(f'Net worth snapshot saved: {snap.snapshot_date.strftime("%B %d, %Y")}', 'success')
|
||||||
return redirect(url_for('reports.index'))
|
return redirect(url_for('reports.index'))
|
||||||
|
|
||||||
|
|
||||||
|
@reports_bp.route('/rebuild-snapshot', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def rebuild_snapshot():
|
||||||
|
"""Recalculate all balances then replace today's snapshot with fresh data."""
|
||||||
|
from app.services.account_service import recalc_all
|
||||||
|
from app.models.net_worth_snapshot import NetWorthSnapshot
|
||||||
|
|
||||||
|
recalc_all()
|
||||||
|
|
||||||
|
# Delete today's snapshot so take_net_worth_snapshot() creates a fresh one
|
||||||
|
today = date.today()
|
||||||
|
existing = NetWorthSnapshot.query.filter_by(snapshot_date=today).first()
|
||||||
|
if existing:
|
||||||
|
from app.extensions import db
|
||||||
|
db.session.delete(existing)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
snap = take_net_worth_snapshot()
|
||||||
|
flash(
|
||||||
|
f'Balances recalculated and snapshot rebuilt for {snap.snapshot_date.strftime("%B %d, %Y")}.',
|
||||||
|
'success'
|
||||||
|
)
|
||||||
|
return redirect(url_for('reports.index'))
|
||||||
|
|||||||
+19
-1
@@ -119,11 +119,29 @@ def _category_choices(txn_type='expense'):
|
|||||||
@settings_bp.route('/')
|
@settings_bp.route('/')
|
||||||
@login_required
|
@login_required
|
||||||
def index():
|
def index():
|
||||||
|
from flask import current_app
|
||||||
upcoming = get_upcoming(days=30)
|
upcoming = get_upcoming(days=30)
|
||||||
rules = RecurringRule.query.filter_by(is_active=True).order_by(RecurringRule.name).all()
|
rules = RecurringRule.query.filter_by(is_active=True).order_by(RecurringRule.name).all()
|
||||||
|
smtp_ok = all([
|
||||||
|
current_app.config.get('SMTP_HOST'),
|
||||||
|
current_app.config.get('SMTP_USER'),
|
||||||
|
current_app.config.get('SMTP_PASSWORD'),
|
||||||
|
current_app.config.get('ALERT_EMAIL'),
|
||||||
|
])
|
||||||
return render_template('settings/index.html',
|
return render_template('settings/index.html',
|
||||||
upcoming=upcoming,
|
upcoming=upcoming,
|
||||||
rules=rules)
|
rules=rules,
|
||||||
|
smtp_ok=smtp_ok,
|
||||||
|
alert_email=current_app.config.get('ALERT_EMAIL', ''))
|
||||||
|
|
||||||
|
|
||||||
|
@settings_bp.route('/recalc-balances', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def recalc_balances():
|
||||||
|
from app.services.account_service import recalc_all
|
||||||
|
recalc_all()
|
||||||
|
flash('All account balances recalculated.', 'success')
|
||||||
|
return redirect(url_for('settings.index'))
|
||||||
|
|
||||||
|
|
||||||
@settings_bp.route('/profile', methods=['GET', 'POST'])
|
@settings_bp.route('/profile', methods=['GET', 'POST'])
|
||||||
|
|||||||
@@ -280,6 +280,23 @@ def sync_all():
|
|||||||
return redirect(url_for('teller.sync_preview_view', teller_account_id=mapped[0].id))
|
return redirect(url_for('teller.sync_preview_view', teller_account_id=mapped[0].id))
|
||||||
|
|
||||||
|
|
||||||
|
# ── Full resync (reset last_sync_date so next sync fetches full history) ─────
|
||||||
|
|
||||||
|
@teller_bp.route('/resync/<int:teller_account_id>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def full_resync(teller_account_id):
|
||||||
|
ta = db.get_or_404(TellerAccount, teller_account_id)
|
||||||
|
ta.last_sync_date = None
|
||||||
|
ta.last_teller_txn_id = None
|
||||||
|
db.session.commit()
|
||||||
|
flash(
|
||||||
|
f'{ta.account_name}: reset to full resync. '
|
||||||
|
f'Next sync will fetch the last 90 days — duplicates will be skipped automatically.',
|
||||||
|
'info'
|
||||||
|
)
|
||||||
|
return redirect(url_for('teller.index'))
|
||||||
|
|
||||||
|
|
||||||
# ── Balance refresh ───────────────────────────────────────────────────────────
|
# ── Balance refresh ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@teller_bp.route('/balance/<int:teller_account_id>', methods=['POST'])
|
@teller_bp.route('/balance/<int:teller_account_id>', methods=['POST'])
|
||||||
|
|||||||
@@ -140,12 +140,21 @@
|
|||||||
<div class="pcard">
|
<div class="pcard">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<span class="pcard-title mb-0">Net Worth History</span>
|
<span class="pcard-title mb-0">Net Worth History</span>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
<form method="POST" action="{{ url_for('reports.manual_snapshot') }}">
|
<form method="POST" action="{{ url_for('reports.manual_snapshot') }}">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<button type="submit" class="btn btn-sm btn-outline-secondary" style="font-size:11px;" title="Save today's snapshot">
|
<button type="submit" class="btn btn-sm btn-outline-secondary" style="font-size:11px;" title="Save today's snapshot">
|
||||||
<i class="bi bi-camera me-1"></i>Snapshot Now
|
<i class="bi bi-camera me-1"></i>Snapshot Now
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
<form method="POST" action="{{ url_for('reports.rebuild_snapshot') }}"
|
||||||
|
onsubmit="return confirm('Recalculate all balances and rebuild today\'s snapshot?\nThis fixes the current net worth data point.')">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-warning" style="font-size:11px;" title="Recalculate all balances then rebuild today's snapshot">
|
||||||
|
<i class="bi bi-arrow-repeat me-1"></i>Rebuild
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if nw_history.count > 1 %}
|
{% if nw_history.count > 1 %}
|
||||||
<div style="position:relative;height:200px;"><canvas id="nwChart"></canvas></div>
|
<div style="position:relative;height:200px;"><canvas id="nwChart"></canvas></div>
|
||||||
|
|||||||
@@ -52,6 +52,53 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Maintenance + Email Alerts -->
|
||||||
|
<div class="row g-3 mt-1">
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<div class="pcard h-100">
|
||||||
|
<div class="pcard-title mb-3">Maintenance</div>
|
||||||
|
<p style="font-size:13px;color:var(--muted);">
|
||||||
|
Recalculate every account balance from scratch using all recorded transactions.
|
||||||
|
Run this after bulk imports or manual adjustments to make sure balances are correct.
|
||||||
|
</p>
|
||||||
|
<form method="POST" action="{{ url_for('settings.recalc_balances') }}"
|
||||||
|
onsubmit="return confirm('Recalculate all account balances now?')">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="bi bi-arrow-repeat me-1"></i>Recalculate All Balances
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<div class="pcard h-100">
|
||||||
|
<div class="pcard-title mb-3">Budget Alert Emails</div>
|
||||||
|
{% if smtp_ok %}
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-2">
|
||||||
|
<i class="bi bi-check-circle-fill text-success"></i>
|
||||||
|
<span style="font-size:13px;">Email alerts enabled → <strong>{{ alert_email }}</strong></span>
|
||||||
|
</div>
|
||||||
|
<p style="font-size:12px;color:var(--muted);">
|
||||||
|
An email is sent automatically when a budget category hits 80% or 100% of its monthly limit.
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-2">
|
||||||
|
<i class="bi bi-x-circle-fill text-danger"></i>
|
||||||
|
<span style="font-size:13px;color:var(--muted);">Email alerts not configured</span>
|
||||||
|
</div>
|
||||||
|
<p style="font-size:12px;color:var(--muted);">
|
||||||
|
Add these to your <code>.env</code> file to enable budget alert emails:
|
||||||
|
</p>
|
||||||
|
<pre style="font-size:11px;background:#f8fafc;padding:10px;border-radius:6px;margin:0;overflow-x:auto;">SMTP_HOST=smtp.gmail.com
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_USER=you@gmail.com
|
||||||
|
SMTP_PASSWORD=your-app-password
|
||||||
|
ALERT_EMAIL=you@gmail.com</pre>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Upcoming recurring -->
|
<!-- Upcoming recurring -->
|
||||||
{% if upcoming %}
|
{% if upcoming %}
|
||||||
<div class="pcard mt-4">
|
<div class="pcard mt-4">
|
||||||
|
|||||||
@@ -84,6 +84,14 @@
|
|||||||
class="btn btn-sm btn-outline-primary" style="font-size:11px;">
|
class="btn btn-sm btn-outline-primary" style="font-size:11px;">
|
||||||
<i class="bi bi-cloud-download me-1"></i>Sync
|
<i class="bi bi-cloud-download me-1"></i>Sync
|
||||||
</a>
|
</a>
|
||||||
|
<form method="POST" action="{{ url_for('teller.full_resync', teller_account_id=ta.id) }}"
|
||||||
|
style="display:inline;"
|
||||||
|
onsubmit="return confirm('Reset sync cursor for {{ ta.account_name }}?\nThe next Sync will re-fetch the last 90 days. Already-imported transactions will be skipped.')">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-warning" style="font-size:11px;" title="Re-fetch full 90-day history on next sync">
|
||||||
|
<i class="bi bi-arrow-counterclockwise"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ url_for('teller.map_accounts', enrollment_id=enrollment.enrollment_id) }}"
|
<a href="{{ url_for('teller.map_accounts', enrollment_id=enrollment.enrollment_id) }}"
|
||||||
class="btn btn-sm btn-outline-warning" style="font-size:11px;">Map Account</a>
|
class="btn btn-sm btn-outline-warning" style="font-size:11px;">Map Account</a>
|
||||||
|
|||||||
Reference in New Issue
Block a user