fix_labels #2
@@ -1047,6 +1047,36 @@ def automations_get():
|
|||||||
return jsonify({'ok': False, 'error': str(exc), 'rules': [], 'history': []}), 500
|
return jsonify({'ok': False, 'error': str(exc), 'rules': [], 'history': []}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get('/automations/export')
|
||||||
|
def automations_export():
|
||||||
|
from ..services import automation_rules
|
||||||
|
profile = preferences.active_profile()
|
||||||
|
if not profile:
|
||||||
|
return jsonify({'ok': False, 'error': 'No profile'}), 400
|
||||||
|
try:
|
||||||
|
# Note: JSON export is profile-scoped and excludes execution history/cooldown state.
|
||||||
|
data = automation_rules.export_rules(profile['id'])
|
||||||
|
return ok({'export': data, 'count': len(data.get('rules') or [])})
|
||||||
|
except Exception as exc:
|
||||||
|
return jsonify({'ok': False, 'error': str(exc)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post('/automations/import')
|
||||||
|
def automations_import():
|
||||||
|
from ..services import automation_rules
|
||||||
|
profile = preferences.active_profile()
|
||||||
|
if not profile:
|
||||||
|
return jsonify({'ok': False, 'error': 'No profile'}), 400
|
||||||
|
try:
|
||||||
|
payload = request.get_json(silent=True) or {}
|
||||||
|
replace = str(request.args.get('replace') or '').lower() in {'1', 'true', 'yes'} or bool(payload.get('replace')) if isinstance(payload, dict) else False
|
||||||
|
# Note: Import appends rules by default, so existing automations remain untouched.
|
||||||
|
imported = automation_rules.import_rules(profile['id'], payload, replace=replace)
|
||||||
|
return ok({'imported': len(imported), 'rules': automation_rules.list_rules(profile['id'])})
|
||||||
|
except Exception as exc:
|
||||||
|
return jsonify({'ok': False, 'error': str(exc)}), 400
|
||||||
|
|
||||||
|
|
||||||
@bp.post('/automations')
|
@bp.post('/automations')
|
||||||
def automations_save():
|
def automations_save():
|
||||||
from ..services import automation_rules
|
from ..services import automation_rules
|
||||||
|
|||||||
@@ -66,6 +66,44 @@ def get_rule(rule_id: int, profile_id: int, user_id: int | None = None) -> dict[
|
|||||||
return _rule_row(row)
|
return _rule_row(row)
|
||||||
|
|
||||||
|
|
||||||
|
def _portable_rule(rule: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
'name': str(rule.get('name') or 'Automation rule'),
|
||||||
|
'enabled': bool(rule.get('enabled', True)),
|
||||||
|
'cooldown_minutes': max(0, int(rule.get('cooldown_minutes') or 0)),
|
||||||
|
'conditions': list(rule.get('conditions') or []),
|
||||||
|
'effects': list(rule.get('effects') or []),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def export_rules(profile_id: int, user_id: int | None = None) -> dict[str, Any]:
|
||||||
|
# Note: Export contains only portable rule definitions, never DB ids or execution history.
|
||||||
|
rules = [_portable_rule(rule) for rule in list_rules(profile_id, user_id)]
|
||||||
|
return {'version': 1, 'app': 'pyTorrent', 'exported_at': utcnow(), 'rules': rules}
|
||||||
|
|
||||||
|
|
||||||
|
def import_rules(profile_id: int, payload: dict[str, Any] | list[Any], user_id: int | None = None, replace: bool = False) -> list[dict[str, Any]]:
|
||||||
|
user_id = user_id or default_user_id()
|
||||||
|
raw_rules = payload if isinstance(payload, list) else payload.get('rules', []) if isinstance(payload, dict) else []
|
||||||
|
if not isinstance(raw_rules, list) or not raw_rules:
|
||||||
|
raise ValueError('Import file does not contain automation rules')
|
||||||
|
if replace:
|
||||||
|
with connect() as conn:
|
||||||
|
# Note: Optional replace is profile-scoped; it does not touch other profiles or history tables.
|
||||||
|
conn.execute('DELETE FROM automation_rules WHERE user_id=? AND profile_id=?', (user_id, profile_id))
|
||||||
|
conn.execute('DELETE FROM automation_rule_state WHERE profile_id=?', (profile_id,))
|
||||||
|
imported = []
|
||||||
|
for raw in raw_rules:
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
continue
|
||||||
|
rule = _portable_rule(raw)
|
||||||
|
rule.pop('id', None)
|
||||||
|
imported.append(save_rule(profile_id, rule, user_id))
|
||||||
|
if not imported:
|
||||||
|
raise ValueError('No valid automation rules found')
|
||||||
|
return imported
|
||||||
|
|
||||||
|
|
||||||
def save_rule(profile_id: int, data: dict[str, Any], user_id: int | None = None) -> dict[str, Any]:
|
def save_rule(profile_id: int, data: dict[str, Any], user_id: int | None = None) -> dict[str, Any]:
|
||||||
user_id = user_id or default_user_id()
|
user_id = user_id or default_user_id()
|
||||||
name = str(data.get('name') or 'Automation rule').strip() or 'Automation rule'
|
name = str(data.get('name') or 'Automation rule').strip() or 'Automation rule'
|
||||||
@@ -111,6 +149,8 @@ def _condition_true(t: dict[str, Any], cond: dict[str, Any]) -> bool:
|
|||||||
if typ == 'completed': return bool(int(t.get('complete') or 0))
|
if typ == 'completed': return bool(int(t.get('complete') or 0))
|
||||||
if typ == 'no_seeds': return int(t.get('seeds') or 0) <= int(cond.get('seeds') or 0)
|
if typ == 'no_seeds': return int(t.get('seeds') or 0) <= int(cond.get('seeds') or 0)
|
||||||
if typ == 'ratio_gte': return float(t.get('ratio') or 0) >= float(cond.get('ratio') or 0)
|
if typ == 'ratio_gte': return float(t.get('ratio') or 0) >= float(cond.get('ratio') or 0)
|
||||||
|
if typ == 'progress_gte': return float(t.get('progress') or 0) >= float(cond.get('progress') or 0)
|
||||||
|
if typ == 'progress_lte': return float(t.get('progress') or 0) <= float(cond.get('progress') or 0)
|
||||||
if typ == 'label_missing': return str(cond.get('label') or '').strip() not in _label_names(t.get('label'))
|
if typ == 'label_missing': return str(cond.get('label') or '').strip() not in _label_names(t.get('label'))
|
||||||
if typ == 'label_has': return str(cond.get('label') or '').strip() in _label_names(t.get('label'))
|
if typ == 'label_has': return str(cond.get('label') or '').strip() in _label_names(t.get('label'))
|
||||||
if typ == 'status': return str(t.get('status') or '').lower() == str(cond.get('status') or '').lower()
|
if typ == 'status': return str(t.get('status') or '').lower() == str(cond.get('status') or '').lower()
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -2293,3 +2293,33 @@ body.mobile-mode .mobile-filter-bar {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.date-readable {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 9.5rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jobs-table {
|
||||||
|
min-width: 980px;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jobs-table th:nth-child(7),
|
||||||
|
.jobs-table td:nth-child(7),
|
||||||
|
.jobs-table th:nth-child(8),
|
||||||
|
.jobs-table td:nth-child(8) {
|
||||||
|
min-width: 10.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jobs-table td:nth-child(5),
|
||||||
|
.jobs-table td:nth-child(9) {
|
||||||
|
max-width: 18rem;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.automation-history-scroll {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user