fix(amas): single continuous progress bar across both import phases
The bar refilled 0->100% once per phase: the parallel fetch phase (the
slow, network-bound one) reported 0..N, then the sequential save phase
reported 0..N again. The client's monotonic guard then pinned the bar at
100% for the whole save phase, so real per-employee progress (with names)
was only visible at the very end, and the bar looked stuck at 0% during
the long fetch.
Report one overall 0-100 percent across both phases (fetch = 0-50, save =
50-100) from the worker and drive the bar from it; show per-phase "N of M"
+ a clear phase label ("Fetching details" / "Saving employees") in the
detail line. Poll every 1s for snappier updates. Bar now advances
continuously and is meaningful from the first tick.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
110a68f8b4
commit
15047ffbaa
|
|
@ -2761,8 +2761,9 @@ def import_bulk_employees(asan_login_name, employees_data, company, create_desig
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|
||||||
# Seed progress immediately so a bar that attaches before the worker emits
|
# Seed progress immediately so a bar that attaches before the worker emits
|
||||||
# its first tick already shows 0 / total instead of an empty "reconnecting".
|
# its first tick already shows the total and a "preparing" state instead of
|
||||||
_set_import_progress(asan_login_name, 0, len(employees_data), phase="queued")
|
# an empty "reconnecting".
|
||||||
|
_set_import_progress(asan_login_name, 0, len(employees_data), phase="queued", percent=0)
|
||||||
|
|
||||||
user = frappe.session.user
|
user = frappe.session.user
|
||||||
|
|
||||||
|
|
@ -2799,6 +2800,7 @@ def get_amas_import_status(asan_login_name):
|
||||||
"total": progress.get("total") or 0,
|
"total": progress.get("total") or 0,
|
||||||
"phase": progress.get("phase"),
|
"phase": progress.get("phase"),
|
||||||
"employee_name": progress.get("employee_name"),
|
"employee_name": progress.get("employee_name"),
|
||||||
|
"percent": progress.get("percent") or 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2875,10 +2877,16 @@ def _import_progress_key(asan_login_name):
|
||||||
return f"amas_import_progress:{asan_login_name}"
|
return f"amas_import_progress:{asan_login_name}"
|
||||||
|
|
||||||
|
|
||||||
def _set_import_progress(asan_login_name, current, total, phase=None, employee_name=None):
|
def _set_import_progress(asan_login_name, current, total, phase=None, employee_name=None, percent=None):
|
||||||
"""Persist the latest progress tick to Redis so a reconnecting list-view bar
|
"""Persist the latest progress tick to Redis so a reconnecting list-view bar
|
||||||
can poll it (see get_amas_import_status). Best-effort: progress reporting
|
can poll it (see get_amas_import_status). Best-effort: progress reporting
|
||||||
must never break the import itself."""
|
must never break the import itself.
|
||||||
|
|
||||||
|
``percent`` is the overall 0-100 progress across BOTH import phases (fetch
|
||||||
|
then save). It drives the bar width directly so the bar advances
|
||||||
|
continuously instead of refilling 0->100 once per phase. ``current``/``total``
|
||||||
|
stay per-phase, for the "N of M" detail text only.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
frappe.cache().set_value(
|
frappe.cache().set_value(
|
||||||
_import_progress_key(asan_login_name),
|
_import_progress_key(asan_login_name),
|
||||||
|
|
@ -2887,6 +2895,7 @@ def _set_import_progress(asan_login_name, current, total, phase=None, employee_n
|
||||||
"total": total,
|
"total": total,
|
||||||
"phase": phase,
|
"phase": phase,
|
||||||
"employee_name": employee_name,
|
"employee_name": employee_name,
|
||||||
|
"percent": percent,
|
||||||
},
|
},
|
||||||
expires_in_sec=3600,
|
expires_in_sec=3600,
|
||||||
)
|
)
|
||||||
|
|
@ -2901,6 +2910,20 @@ def _clear_import_progress(asan_login_name):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _overall_percent(phase, current, total):
|
||||||
|
"""Collapse the two import phases into one continuous 0-100 bar:
|
||||||
|
fetch = first half (0-50), save = second half (50-100). 'queued' sits at 0.
|
||||||
|
Keeps the bar moving forward instead of refilling once per phase."""
|
||||||
|
if not total or total <= 0:
|
||||||
|
return 0
|
||||||
|
frac = max(0.0, min(1.0, current / total))
|
||||||
|
if phase == "fetch":
|
||||||
|
return round(frac * 50.0, 1)
|
||||||
|
if phase == "save":
|
||||||
|
return round(50.0 + frac * 50.0, 1)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def _process_bulk_employees_import(asan_login_name, employees, company, create_designation, user):
|
def _process_bulk_employees_import(asan_login_name, employees, company, create_designation, user):
|
||||||
"""Background job: import employees with realtime progress.
|
"""Background job: import employees with realtime progress.
|
||||||
|
|
||||||
|
|
@ -2997,10 +3020,11 @@ def _process_bulk_employees_import(asan_login_name, employees, company, create_d
|
||||||
or outcome["emp_data"].get("identification_number")
|
or outcome["emp_data"].get("identification_number")
|
||||||
or "Employee"
|
or "Employee"
|
||||||
)
|
)
|
||||||
_set_import_progress(asan_login_name, completed, total, "fetch", emp_name)
|
pct = _overall_percent("fetch", completed, total)
|
||||||
|
_set_import_progress(asan_login_name, completed, total, "fetch", emp_name, pct)
|
||||||
frappe.publish_realtime(
|
frappe.publish_realtime(
|
||||||
"amas_import_progress",
|
"amas_import_progress",
|
||||||
{"current": completed, "total": total, "employee_name": emp_name, "phase": "fetch"},
|
{"current": completed, "total": total, "employee_name": emp_name, "phase": "fetch", "percent": pct},
|
||||||
user=user,
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -3060,10 +3084,11 @@ def _process_bulk_employees_import(asan_login_name, employees, company, create_d
|
||||||
|
|
||||||
if outcome["error"]:
|
if outcome["error"]:
|
||||||
errors.append({"employee": emp_name, "error": outcome["error"]})
|
errors.append({"employee": emp_name, "error": outcome["error"]})
|
||||||
_set_import_progress(asan_login_name, idx + 1, total, "save", emp_name)
|
pct = _overall_percent("save", idx + 1, total)
|
||||||
|
_set_import_progress(asan_login_name, idx + 1, total, "save", emp_name, pct)
|
||||||
frappe.publish_realtime(
|
frappe.publish_realtime(
|
||||||
"amas_import_progress",
|
"amas_import_progress",
|
||||||
{"current": idx + 1, "total": total, "employee_name": emp_name, "phase": "save"},
|
{"current": idx + 1, "total": total, "employee_name": emp_name, "phase": "save", "percent": pct},
|
||||||
user=user,
|
user=user,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
@ -3087,10 +3112,11 @@ def _process_bulk_employees_import(asan_login_name, employees, company, create_d
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
errors.append({"employee": emp_name, "error": str(e)})
|
errors.append({"employee": emp_name, "error": str(e)})
|
||||||
|
|
||||||
_set_import_progress(asan_login_name, idx + 1, total, "save", emp_name)
|
pct = _overall_percent("save", idx + 1, total)
|
||||||
|
_set_import_progress(asan_login_name, idx + 1, total, "save", emp_name, pct)
|
||||||
frappe.publish_realtime(
|
frappe.publish_realtime(
|
||||||
"amas_import_progress",
|
"amas_import_progress",
|
||||||
{"current": idx + 1, "total": total, "employee_name": emp_name, "phase": "save"},
|
{"current": idx + 1, "total": total, "employee_name": emp_name, "phase": "save", "percent": pct},
|
||||||
user=user,
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -714,42 +714,55 @@ function attach_amas_import_listeners(listview, asan_login_name, total_hint) {
|
||||||
// fire before the worker has even started (the running flag is set
|
// fire before the worker has even started (the running flag is set
|
||||||
// server-side a beat after import_bulk_employees is called).
|
// server-side a beat after import_bulk_employees is called).
|
||||||
let sawRunning = (total > 0);
|
let sawRunning = (total > 0);
|
||||||
// Highest "current" rendered so far. Realtime and polling both feed the bar;
|
// Highest overall percent (0-100) rendered so far. Realtime and polling both
|
||||||
// this keeps it monotonic so a stale poll can't rewind a fresher realtime
|
// feed the bar; this keeps it monotonic so a stale poll can't rewind a
|
||||||
// tick (or vice-versa).
|
// fresher realtime tick (or vice-versa).
|
||||||
let lastCurrent = 0;
|
let lastPercent = 0;
|
||||||
let knownTotal = total || 0;
|
|
||||||
|
|
||||||
let dialog = frappe.show_progress(
|
let dialog = frappe.show_progress(
|
||||||
__('Importing from ƏMAS'), 0, total,
|
__('Importing from ƏMAS'), 0, 100,
|
||||||
total
|
total
|
||||||
? __('Starting import of {0} employees...', [total])
|
? __('Starting import of {0} employees...', [total])
|
||||||
: __('Importing employees, please wait...')
|
: __('Preparing import...')
|
||||||
);
|
);
|
||||||
ensure_cancel_button(dialog);
|
ensure_cancel_button(dialog);
|
||||||
|
|
||||||
// Single entry point for moving the bar, fed by both the realtime handler
|
const phaseLabel = function(phase) {
|
||||||
// and the status poll. Never regresses; tolerates an unknown total (0).
|
if (phase === 'save') return __('Saving employees');
|
||||||
const renderProgress = function(current, totalArg, phase, employee_name) {
|
if (phase === 'fetch') return __('Fetching details');
|
||||||
if (importFinished) return;
|
return __('Preparing import...');
|
||||||
if (totalArg && totalArg > 0) knownTotal = totalArg;
|
};
|
||||||
if (typeof current === 'number' && current > lastCurrent) lastCurrent = current;
|
|
||||||
|
|
||||||
const is_save = phase === 'save';
|
// The bar tracks ONE overall percent across both import phases (fetch =
|
||||||
const verb = is_save ? __('Saving: ') : __('Fetching: ');
|
// 0-50, save = 50-100), computed server-side. Fall back to deriving it the
|
||||||
|
// same way if an older worker sends no `percent`.
|
||||||
|
const overallPercent = function(percent, current, total, phase) {
|
||||||
|
if (typeof percent === 'number' && percent >= 0) return percent;
|
||||||
|
if (!(total > 0)) return 0;
|
||||||
|
const frac = Math.max(0, Math.min(1, current / total));
|
||||||
|
if (phase === 'save') return 50 + frac * 50;
|
||||||
|
if (phase === 'fetch') return frac * 50;
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Single entry point for moving the bar, fed by both the realtime handler
|
||||||
|
// and the status poll. Never regresses.
|
||||||
|
const renderProgress = function(percent, current, total, phase, employee_name) {
|
||||||
|
if (importFinished) return;
|
||||||
|
const pct = overallPercent(percent, current, total, phase);
|
||||||
|
if (pct > lastPercent) lastPercent = pct;
|
||||||
|
|
||||||
|
const label = phaseLabel(phase);
|
||||||
let detail;
|
let detail;
|
||||||
if (employee_name) {
|
if (phase === 'queued' || !(total > 0)) {
|
||||||
detail = verb + employee_name;
|
detail = label;
|
||||||
} else if (knownTotal > 0) {
|
} else if (employee_name) {
|
||||||
detail = verb + __('{0} of {1}', [lastCurrent, knownTotal]);
|
detail = label + ': ' + employee_name + ' (' + current + '/' + total + ')';
|
||||||
} else {
|
} else {
|
||||||
// Running, but no count yet (first tick not in, or server hasn't
|
detail = label + ': ' + __('{0} of {1}', [current, total]);
|
||||||
// populated progress). Keep it reassuring rather than alarming.
|
|
||||||
detail = __('Importing employees, please wait...');
|
|
||||||
}
|
}
|
||||||
// Guard against total=0 (NaN width); show_progress reuses the dialog.
|
|
||||||
const d = frappe.show_progress(
|
const d = frappe.show_progress(
|
||||||
__('Importing from ƏMAS'), lastCurrent, knownTotal > 0 ? knownTotal : 1, detail
|
__('Importing from ƏMAS'), Math.round(lastPercent), 100, detail
|
||||||
);
|
);
|
||||||
ensure_cancel_button(d);
|
ensure_cancel_button(d);
|
||||||
};
|
};
|
||||||
|
|
@ -758,7 +771,7 @@ function attach_amas_import_listeners(listview, asan_login_name, total_hint) {
|
||||||
frappe.realtime.on('amas_import_progress', function(data) {
|
frappe.realtime.on('amas_import_progress', function(data) {
|
||||||
if (importFinished) return;
|
if (importFinished) return;
|
||||||
sawRunning = true;
|
sawRunning = true;
|
||||||
renderProgress(data.current, data.total, data.phase, data.employee_name);
|
renderProgress(data.percent, data.current, data.total, data.phase, data.employee_name);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Polling fallback. Realtime events are transient — anything published
|
// Polling fallback. Realtime events are transient — anything published
|
||||||
|
|
@ -787,7 +800,7 @@ function attach_amas_import_listeners(listview, asan_login_name, total_hint) {
|
||||||
const m = r.message || {};
|
const m = r.message || {};
|
||||||
if (m.running) {
|
if (m.running) {
|
||||||
sawRunning = true;
|
sawRunning = true;
|
||||||
renderProgress(m.current, m.total, m.phase, m.employee_name);
|
renderProgress(m.percent, m.current, m.total, m.phase, m.employee_name);
|
||||||
} else if (sawRunning) {
|
} else if (sawRunning) {
|
||||||
// We watched it run and now it's gone — the realtime
|
// We watched it run and now it's gone — the realtime
|
||||||
// 'complete' was missed. Wrap up gracefully.
|
// 'complete' was missed. Wrap up gracefully.
|
||||||
|
|
@ -798,10 +811,10 @@ function attach_amas_import_listeners(listview, asan_login_name, total_hint) {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (amasPollTimer) clearInterval(amasPollTimer);
|
if (amasPollTimer) clearInterval(amasPollTimer);
|
||||||
amasPollTimer = setInterval(pollStatus, 2000);
|
amasPollTimer = setInterval(pollStatus, 1000);
|
||||||
// Reconnect path (no total hint): the caller already confirmed an import is
|
// Reconnect path (no total hint): the caller already confirmed an import is
|
||||||
// running, so seed the bar from real numbers immediately instead of waiting
|
// running, so seed the bar from real numbers immediately instead of waiting
|
||||||
// a full poll interval on the bare "reconnecting" text.
|
// a full poll interval on the bare "preparing" text.
|
||||||
if (!total) pollStatus();
|
if (!total) pollStatus();
|
||||||
|
|
||||||
frappe.realtime.off('amas_import_complete');
|
frappe.realtime.off('amas_import_complete');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue