记录原始数据变更操作日志

This commit is contained in:
Codex
2026-06-16 04:49:31 +08:00
parent a1a8466f6b
commit c1d3d19dfc
6 changed files with 202 additions and 42 deletions
+100 -21
View File
@@ -524,7 +524,7 @@ def create_teacher(path: Path, teacher: Teacher) -> dict:
prune_data_backups(backup_dir.parent) prune_data_backups(backup_dir.parent)
except OSError: except OSError:
pass pass
return {"teacher": teacher_to_dict(teacher), "backup_id": backup_dir.name} return {"teacher": teacher_to_dict(teacher), "backup_id": backup_dir.name, "operation": "新增老师档案"}
def update_teacher(path: Path, old_teacher_id: str, teacher: Teacher) -> dict: def update_teacher(path: Path, old_teacher_id: str, teacher: Teacher) -> dict:
@@ -542,7 +542,7 @@ def update_teacher(path: Path, old_teacher_id: str, teacher: Teacher) -> dict:
prune_data_backups(backup_dir.parent) prune_data_backups(backup_dir.parent)
except OSError: except OSError:
pass pass
return {"teacher": teacher_to_dict(teacher), "backup_id": backup_dir.name} return {"teacher": teacher_to_dict(teacher), "backup_id": backup_dir.name, "operation": "修改老师档案"}
def teacher_to_dict(teacher: Teacher) -> dict: def teacher_to_dict(teacher: Teacher) -> dict:
@@ -590,7 +590,7 @@ def create_account(path: Path, account: Account) -> dict:
prune_data_backups(backup_dir.parent) prune_data_backups(backup_dir.parent)
except OSError: except OSError:
pass pass
return {"account": account_to_dict(account), "backup_id": backup_dir.name} return {"account": account_to_dict(account), "backup_id": backup_dir.name, "operation": "新增课时账户"}
def update_account(path: Path, old_student_id: str, account: Account) -> dict: def update_account(path: Path, old_student_id: str, account: Account) -> dict:
@@ -608,7 +608,7 @@ def update_account(path: Path, old_student_id: str, account: Account) -> dict:
prune_data_backups(backup_dir.parent) prune_data_backups(backup_dir.parent)
except OSError: except OSError:
pass pass
return {"account": account_to_dict(account), "backup_id": backup_dir.name} return {"account": account_to_dict(account), "backup_id": backup_dir.name, "operation": "修改课时账户"}
def render_accounts_text(path: Path, accounts: list[Account]) -> str: def render_accounts_text(path: Path, accounts: list[Account]) -> str:
@@ -691,7 +691,7 @@ def register_class_record_lines(
prune_data_backups(backup_dir.parent) prune_data_backups(backup_dir.parent)
except OSError: except OSError:
pass pass
return {"registered": len(record_lines), "lines": record_lines, "backup_id": backup_dir.name} return {"registered": len(record_lines), "lines": record_lines, "backup_id": backup_dir.name, "operation": "登记上课记录"}
def register_payment_lines( def register_payment_lines(
@@ -726,7 +726,7 @@ def register_payment_lines(
prune_data_backups(backup_dir.parent) prune_data_backups(backup_dir.parent)
except OSError: except OSError:
pass pass
return {"registered": len(registered), "lines": registered, "backup_id": backup_dir.name} return {"registered": len(registered), "lines": registered, "backup_id": backup_dir.name, "operation": "登记缴费记录"}
def default_admin_tasks() -> dict: def default_admin_tasks() -> dict:
@@ -1358,18 +1358,96 @@ def append_operation_log(path: Path, operation: str, status: str, **fields: obje
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
now = datetime.now().isoformat(timespec="seconds") now = datetime.now().isoformat(timespec="seconds")
log_id = f"{datetime.now().strftime('%Y%m%d%H%M%S')}-{sha1_text(json.dumps(fields, ensure_ascii=False, sort_keys=True), 8)}" log_id = f"{datetime.now().strftime('%Y%m%d%H%M%S')}-{sha1_text(json.dumps(fields, ensure_ascii=False, sort_keys=True), 8)}"
row = { row = localize_operation_log_item({
"id": log_id, "id": log_id,
"created_at": now, "created_at": now,
"operation": operation, "operation": operation,
"status": status, "status": status,
**fields, **fields,
} })
with path.open("a", encoding="utf-8") as handle: with path.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(row, ensure_ascii=False, sort_keys=True) + "\n") handle.write(json.dumps(row, ensure_ascii=False, sort_keys=True) + "\n")
return log_id return log_id
OPERATION_LABELS = {
"history_course_summary_import": "历史课程小结导入",
"course_summary_manual_register": "课程小结登记",
"course_summary_ingest": "课程小结接收",
"admin_task_approve": "审核批准",
"admin_task_reject": "审核驳回",
"admin_course_summary_update_time": "课程小结补齐时间",
"admin_course_summary_delete": "课程小结删除",
"register-class-records": "登记上课记录",
"register-payments": "登记缴费记录",
"admin-create-account": "新增课时账户",
"admin-update-account": "修改课时账户",
"admin-create-teacher": "新增老师档案",
"admin-update-teacher": "修改老师档案",
"admin-approve-correction": "审核批准上课记录纠错",
"admin-approve-deletion": "审核批准上课记录删除",
"admin-update-course-summary-time": "课程小结补齐时间",
"admin-delete-course-summary": "课程小结删除",
}
STATUS_LABELS = {
"completed": "完成",
"approved": "已批准",
"rejected": "已驳回",
"updated": "已更新",
"deleted": "已删除",
"duplicate": "重复",
"auto_registered": "自动入账",
"review": "待审核",
"conflict": "冲突",
"pending": "待处理",
}
TYPE_LABELS = {
"class_record_correction": "上课记录纠错",
"class_record_deletion": "上课记录删除",
"course_summary_review": "课程小结审核",
}
def localize_operation_log_item(item: dict) -> dict:
row = dict(item)
operation = str(row.get("operation") or "")
status = str(row.get("status") or "")
task_type = str(row.get("task_type") or "")
row["operation"] = OPERATION_LABELS.get(operation, operation)
row["status"] = STATUS_LABELS.get(status, status)
if task_type:
row["task_type"] = TYPE_LABELS.get(task_type, task_type)
if row.get("message"):
row["message"] = str(row["message"])
return row
def migrate_operation_log_labels(path: Path) -> dict:
if not path.exists():
return {"updated": 0, "path": str(path)}
lines = path.read_text(encoding="utf-8").splitlines()
updated_lines: list[str] = []
changed = 0
for raw_line in lines:
if not raw_line.strip():
updated_lines.append(raw_line)
continue
try:
item = json.loads(raw_line)
except json.JSONDecodeError:
updated_lines.append(raw_line)
continue
localized = localize_operation_log_item(item)
if localized != item:
changed += 1
updated_lines.append(json.dumps(localized, ensure_ascii=False, sort_keys=True))
if changed:
atomic_write_text(path, "\n".join(updated_lines) + "\n")
return {"updated": changed, "path": str(path)}
def list_operation_logs(path: Path, limit: int = 100, operation: str = "", status_filter: str = "", student: str = "") -> dict: def list_operation_logs(path: Path, limit: int = 100, operation: str = "", status_filter: str = "", student: str = "") -> dict:
rows: list[dict] = [] rows: list[dict] = []
if path.exists(): if path.exists():
@@ -1380,6 +1458,7 @@ def list_operation_logs(path: Path, limit: int = 100, operation: str = "", statu
item = json.loads(raw_line) item = json.loads(raw_line)
except json.JSONDecodeError: except json.JSONDecodeError:
continue continue
item = localize_operation_log_item(item)
if operation and item.get("operation") != operation: if operation and item.get("operation") != operation:
continue continue
if status_filter and item.get("status") != status_filter: if status_filter and item.get("status") != status_filter:
@@ -1948,8 +2027,8 @@ def register_course_summary_texts(
result["duplicates"] += 1 result["duplicates"] += 1
log_id = append_operation_log( log_id = append_operation_log(
operation_logs_path, operation_logs_path,
"course_summary_manual_register", "课程小结登记",
"duplicate", "重复",
source_id=source_id, source_id=source_id,
student=normalized["student"], student=normalized["student"],
) )
@@ -1987,8 +2066,8 @@ def register_course_summary_texts(
write_course_summary_state(state_path, state) write_course_summary_state(state_path, state)
log_id = append_operation_log( log_id = append_operation_log(
operation_logs_path, operation_logs_path,
"course_summary_manual_register", "课程小结登记",
"auto_registered", "自动入账",
source_id=source_id, source_id=source_id,
student=normalized["student"], student=normalized["student"],
teacher=normalized.get("teacher", ""), teacher=normalized.get("teacher", ""),
@@ -2012,8 +2091,8 @@ def register_course_summary_texts(
student = str((normalized or raw or {}).get("student") or "") student = str((normalized or raw or {}).get("student") or "")
log_id = append_operation_log( log_id = append_operation_log(
operation_logs_path, operation_logs_path,
"course_summary_manual_register", "课程小结登记",
"rejected", "已驳回",
source_id=source_id, source_id=source_id,
student=student, student=student,
error=str(exc), error=str(exc),
@@ -2067,8 +2146,8 @@ def ingest_course_summaries(
result["duplicates"] += 1 result["duplicates"] += 1
log_id = append_operation_log( log_id = append_operation_log(
operation_logs_path, operation_logs_path,
"course_summary_ingest", "课程小结接收",
"duplicate", "重复",
batch_id=batch_id, batch_id=batch_id,
source_id=source_id, source_id=source_id,
student=normalized["student"], student=normalized["student"],
@@ -2090,12 +2169,12 @@ def ingest_course_summaries(
saved_path=str(saved.get("path") or ""), saved_path=str(saved.get("path") or ""),
) )
result["review_pending"] += 1 result["review_pending"] += 1
status_value = "review" status_value = "待审核"
task_id = task.get("id") task_id = task.get("id")
else: else:
register_result = register_class_record_lines(classnotes_path, accounts_path, line=proposed_line) register_result = register_class_record_lines(classnotes_path, accounts_path, line=proposed_line)
result["auto_registered"] += 1 result["auto_registered"] += 1
status_value = "auto_registered" status_value = "自动入账"
task_id = None task_id = None
backup_id = str(register_result.get("backup_id") or "") backup_id = str(register_result.get("backup_id") or "")
@@ -2103,7 +2182,7 @@ def ingest_course_summaries(
seen_semantic_keys.add(semantic_key) seen_semantic_keys.add(semantic_key)
log_id = append_operation_log( log_id = append_operation_log(
operation_logs_path, operation_logs_path,
"course_summary_ingest", "课程小结接收",
status_value, status_value,
batch_id=batch_id, batch_id=batch_id,
source_id=source_id, source_id=source_id,
@@ -2131,8 +2210,8 @@ def ingest_course_summaries(
source_id = str((normalized or raw).get("source_id") or "") source_id = str((normalized or raw).get("source_id") or "")
log_id = append_operation_log( log_id = append_operation_log(
operation_logs_path, operation_logs_path,
"course_summary_ingest", "课程小结接收",
"rejected", "已驳回",
batch_id=batch_id, batch_id=batch_id,
source_id=source_id, source_id=source_id,
student=str((normalized or raw).get("student") or ""), student=str((normalized or raw).get("student") or ""),
+58
View File
@@ -20,9 +20,11 @@ from ..data import (
DuplicateRecordError, DuplicateRecordError,
account_summary, account_summary,
account_to_dict, account_to_dict,
append_operation_log,
create_account, create_account,
create_teacher, create_teacher,
filter_accounts, filter_accounts,
parse_class_record_line,
register_class_record_lines, register_class_record_lines,
register_course_summary_texts, register_course_summary_texts,
register_payment_lines, register_payment_lines,
@@ -47,6 +49,16 @@ async def register_class_records(request: Request, _user: str = Depends(verify_a
lines=payload.lines, lines=payload.lines,
line=payload.line, line=payload.line,
) )
for line in result.get("lines", []):
record = parse_class_record_line(str(line))
append_operation_log(
OPERATION_LOGS_PATH,
str(result.get("operation") or "登记上课记录"),
"完成",
student=record.student,
proposed_line=str(line),
backup_id=str(result.get("backup_id") or ""),
)
except DuplicateRecordError as exc: except DuplicateRecordError as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc raise HTTPException(status_code=409, detail=str(exc)) from exc
except ValueError as exc: except ValueError as exc:
@@ -60,6 +72,16 @@ async def register_payments(request: Request, _user: str = Depends(verify_admin_
payload = await read_register_payload(request) payload = await read_register_payload(request)
with write_lock: with write_lock:
result = register_payment_lines(ACCOUNTS_PATH, lines=payload.lines, line=payload.line) result = register_payment_lines(ACCOUNTS_PATH, lines=payload.lines, line=payload.line)
for line in result.get("lines", []):
student = str(line).split("-", 1)[0]
append_operation_log(
OPERATION_LOGS_PATH,
str(result.get("operation") or "登记缴费记录"),
"完成",
student=student,
proposed_line=str(line),
backup_id=str(result.get("backup_id") or ""),
)
except ValueError as exc: except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc raise HTTPException(status_code=400, detail=str(exc)) from exc
return {"ok": True, **result} return {"ok": True, **result}
@@ -144,6 +166,15 @@ def admin_create_teacher(payload: TeacherPayload, _user: str = Depends(verify_ad
try: try:
with write_lock: with write_lock:
result = create_teacher(TEACHERS_PATH, payload_to_teacher(payload)) result = create_teacher(TEACHERS_PATH, payload_to_teacher(payload))
log_operation = str(result.get("operation") or "新增老师档案")
append_operation_log(
OPERATION_LOGS_PATH,
log_operation,
"完成",
teacher_id=str(result.get("teacher", {}).get("teacher_id") or ""),
teacher=str(result.get("teacher", {}).get("name") or ""),
backup_id=str(result.get("backup_id") or ""),
)
except ValueError as exc: except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc raise HTTPException(status_code=400, detail=str(exc)) from exc
return {"ok": True, **result} return {"ok": True, **result}
@@ -158,6 +189,15 @@ def admin_update_teacher(teacher_id: str, payload: TeacherPayload, _user: str =
teacher_id, teacher_id,
payload_to_teacher(payload, teacher_id=payload.teacher_id.strip() or teacher_id), payload_to_teacher(payload, teacher_id=payload.teacher_id.strip() or teacher_id),
) )
log_operation = str(result.get("operation") or "修改老师档案")
append_operation_log(
OPERATION_LOGS_PATH,
log_operation,
"完成",
teacher_id=str(result.get("teacher", {}).get("teacher_id") or ""),
teacher=str(result.get("teacher", {}).get("name") or ""),
backup_id=str(result.get("backup_id") or ""),
)
except ValueError as exc: except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc raise HTTPException(status_code=400, detail=str(exc)) from exc
return {"ok": True, **result} return {"ok": True, **result}
@@ -168,6 +208,15 @@ def admin_create_account(payload: AccountPayload, _user: str = Depends(verify_ad
try: try:
with write_lock: with write_lock:
result = create_account(ACCOUNTS_PATH, payload_to_account(payload)) result = create_account(ACCOUNTS_PATH, payload_to_account(payload))
log_operation = str(result.get("operation") or "新增课时账户")
append_operation_log(
OPERATION_LOGS_PATH,
log_operation,
"完成",
student_id=str(result.get("account", {}).get("student_id") or ""),
student=str(result.get("account", {}).get("student") or ""),
backup_id=str(result.get("backup_id") or ""),
)
except ValueError as exc: except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc raise HTTPException(status_code=400, detail=str(exc)) from exc
return {"ok": True, **result} return {"ok": True, **result}
@@ -178,6 +227,15 @@ def admin_update_account(student_id: str, payload: AccountPayload, _user: str =
try: try:
with write_lock: with write_lock:
result = update_account(ACCOUNTS_PATH, student_id, payload_to_account(payload)) result = update_account(ACCOUNTS_PATH, student_id, payload_to_account(payload))
log_operation = str(result.get("operation") or "修改课时账户")
append_operation_log(
OPERATION_LOGS_PATH,
log_operation,
"完成",
student_id=str(result.get("account", {}).get("student_id") or ""),
student=str(result.get("account", {}).get("student") or ""),
backup_id=str(result.get("backup_id") or ""),
)
except ValueError as exc: except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc raise HTTPException(status_code=400, detail=str(exc)) from exc
return {"ok": True, **result} return {"ok": True, **result}
+10 -8
View File
@@ -17,6 +17,7 @@ from ..data import (
delete_course_summary, delete_course_summary,
list_admin_tasks, list_admin_tasks,
list_operation_logs, list_operation_logs,
migrate_operation_log_labels,
query_course_summaries, query_course_summaries,
reject_admin_task, reject_admin_task,
update_course_summary_time, update_course_summary_time,
@@ -46,6 +47,7 @@ def admin_operation_logs(
student: str = Query(""), student: str = Query(""),
_user: str = Depends(verify_admin_auth), _user: str = Depends(verify_admin_auth),
): ):
migrate_operation_log_labels(OPERATION_LOGS_PATH)
return list_operation_logs( return list_operation_logs(
OPERATION_LOGS_PATH, OPERATION_LOGS_PATH,
limit=limit, limit=limit,
@@ -91,8 +93,8 @@ def admin_approve_task(task_id: int, _user: str = Depends(verify_admin_auth)):
task = result.get("task", {}) task = result.get("task", {})
append_operation_log( append_operation_log(
OPERATION_LOGS_PATH, OPERATION_LOGS_PATH,
"admin_task_approve", "审核批准",
"approved", "已批准",
task_id=task_id, task_id=task_id,
task_type=str(task.get("type") or ""), task_type=str(task.get("type") or ""),
student=str(task.get("student") or task.get("corrected", {}).get("student") or ""), student=str(task.get("student") or task.get("corrected", {}).get("student") or ""),
@@ -111,8 +113,8 @@ def admin_reject_task(task_id: int, _user: str = Depends(verify_admin_auth)):
task = reject_admin_task(ADMIN_TASKS_PATH, task_id) task = reject_admin_task(ADMIN_TASKS_PATH, task_id)
append_operation_log( append_operation_log(
OPERATION_LOGS_PATH, OPERATION_LOGS_PATH,
"admin_task_reject", "审核驳回",
"rejected", "已驳回",
task_id=task_id, task_id=task_id,
task_type=str(task.get("type") or ""), task_type=str(task.get("type") or ""),
student=str(task.get("student") or task.get("corrected", {}).get("student") or ""), student=str(task.get("student") or task.get("corrected", {}).get("student") or ""),
@@ -130,8 +132,8 @@ def admin_update_course_summary_time(summary_id: str, payload: dict, _user: str
result = update_course_summary_time(COURSE_SUMMARIES_ROOT, summary_id, str(payload.get("time_range") or "")) result = update_course_summary_time(COURSE_SUMMARIES_ROOT, summary_id, str(payload.get("time_range") or ""))
append_operation_log( append_operation_log(
OPERATION_LOGS_PATH, OPERATION_LOGS_PATH,
"admin_course_summary_update_time", "课程小结补齐时间",
"updated", "已更新",
summary_id=summary_id, summary_id=summary_id,
time_range=str(payload.get("time_range") or ""), time_range=str(payload.get("time_range") or ""),
backup_id=str(result.get("backup_id") or ""), backup_id=str(result.get("backup_id") or ""),
@@ -148,8 +150,8 @@ def admin_delete_course_summary(summary_id: str, _user: str = Depends(verify_adm
result = delete_course_summary(COURSE_SUMMARIES_ROOT, summary_id) result = delete_course_summary(COURSE_SUMMARIES_ROOT, summary_id)
append_operation_log( append_operation_log(
OPERATION_LOGS_PATH, OPERATION_LOGS_PATH,
"admin_course_summary_delete", "课程小结删除",
"deleted", "已删除",
summary_id=summary_id, summary_id=summary_id,
backup_id=str(result.get("backup_id") or ""), backup_id=str(result.get("backup_id") or ""),
) )
+20 -8
View File
@@ -330,17 +330,29 @@
<div class="quick-actions"> <div class="quick-actions">
<select id="logOperation" aria-label="操作类型筛选"> <select id="logOperation" aria-label="操作类型筛选">
<option value="">全部操作</option> <option value="">全部操作</option>
<option value="course_summary_ingest">小结接收</option> <option value="登记上课记录">登记上课记录</option>
<option value="admin_task_approve">审核批准</option> <option value="登记缴费记录">登记缴费记录</option>
<option value="admin_task_reject">审核驳回</option> <option value="新增课时账户">新增课时账户</option>
<option value="修改课时账户">修改课时账户</option>
<option value="新增老师档案">新增老师档案</option>
<option value="修改老师档案">修改老师档案</option>
<option value="课程小结接收">课程小结接收</option>
<option value="课程小结登记">课程小结登记</option>
<option value="审核批准">审核批准</option>
<option value="审核驳回">审核驳回</option>
<option value="课程小结补齐时间">课程小结补齐时间</option>
<option value="课程小结删除">课程小结删除</option>
</select> </select>
<select id="logStatus" aria-label="操作结果筛选"> <select id="logStatus" aria-label="操作结果筛选">
<option value="">全部结果</option> <option value="">全部结果</option>
<option value="auto_registered">自动入账</option> <option value="完成">完成</option>
<option value="review">待审核</option> <option value="自动入账">自动入账</option>
<option value="duplicate">重复</option> <option value="待审核">待审核</option>
<option value="rejected">失败/驳回</option> <option value="重复">重复</option>
<option value="approved">已批准</option> <option value="已驳回">失败/驳回</option>
<option value="已批准">已批准</option>
<option value="已更新">已更新</option>
<option value="已删除">已删除</option>
</select> </select>
</div> </div>
</div> </div>
+7 -3
View File
@@ -134,9 +134,9 @@ function statusClass(status) {
} }
function taskStatusClass(status) { function taskStatusClass(status) {
if (status === "approved" || status === "auto_registered") return "normal"; if (["approved", "auto_registered", "已批准", "自动入账", "完成", "已更新", "已删除"].includes(status)) return "normal";
if (status === "rejected" || status === "duplicate") return "closed"; if (["rejected", "duplicate", "已驳回", "重复"].includes(status)) return "closed";
if (status === "conflict") return "debt"; if (status === "conflict" || status === "冲突") return "debt";
return "warning"; return "warning";
} }
@@ -695,8 +695,12 @@ async function deleteCourseSummary(summaryId) {
function renderLogDetail(item) { function renderLogDetail(item) {
const details = []; const details = [];
if (item.task_type) details.push(`类型:${item.task_type}`);
if (item.source_id) details.push(`来源:${item.source_id}`); if (item.source_id) details.push(`来源:${item.source_id}`);
if (item.student_id) details.push(`学生ID${item.student_id}`);
if (item.teacher_id) details.push(`教师ID${item.teacher_id}`);
if (item.teacher || item.subject) details.push(`老师/科目:${item.teacher || ""} ${item.subject || ""}`.trim()); if (item.teacher || item.subject) details.push(`老师/科目:${item.teacher || ""} ${item.subject || ""}`.trim());
if (item.time_range) details.push(`时间:${item.time_range}`);
if (item.proposed_line) details.push(`记录:${item.proposed_line}`); if (item.proposed_line) details.push(`记录:${item.proposed_line}`);
if (Array.isArray(item.reasons) && item.reasons.length) details.push(`原因:${item.reasons.join("")}`); if (Array.isArray(item.reasons) && item.reasons.length) details.push(`原因:${item.reasons.join("")}`);
if (item.error) details.push(`错误:${item.error}`); if (item.error) details.push(`错误:${item.error}`);
+7 -2
View File
@@ -538,8 +538,10 @@ textarea:focus {
.table-wrap { .table-wrap {
max-height: calc(100vh - 292px); max-height: calc(100vh - 292px);
overflow: auto; overflow-x: auto;
overscroll-behavior: contain; overflow-y: auto;
overscroll-behavior-x: contain;
overscroll-behavior-y: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
} }
@@ -1047,6 +1049,9 @@ td {
.table-wrap { .table-wrap {
max-height: none; max-height: none;
overflow-x: auto;
overflow-y: visible;
overscroll-behavior-y: auto;
} }
.inline-account-grid { .inline-account-grid {