Kgshop commited on
Commit
d6a8eff
·
verified ·
1 Parent(s): 1ee97e8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +263 -75
app.py CHANGED
@@ -177,7 +177,7 @@ TEMPLATE = """
177
  <head>
178
  <meta charset="UTF-8">
179
  <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no, user-scalable=no, viewport-fit=cover">
180
- <title>Druzhba Бонусы</title>
181
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
182
  <link rel="preconnect" href="https://fonts.googleapis.com">
183
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
@@ -186,6 +186,7 @@ TEMPLATE = """
186
  :root {
187
  --brand-yellow: #FFC107;
188
  --brand-black: #101010;
 
189
  --card-bg: #1c1c1e;
190
  --text-color: #ffffff;
191
  --text-secondary-color: #a0a0a0;
@@ -195,6 +196,8 @@ TEMPLATE = """
195
  --font-family: 'Manrope', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
196
  --shadow-color: rgba(255, 193, 7, 0.15);
197
  --shadow-glow: 0 0 35px var(--shadow-color);
 
 
198
  }
199
  * { box-sizing: border-box; margin: 0; padding: 0; }
200
  html, body {
@@ -213,7 +216,7 @@ TEMPLATE = """
213
  margin: 0 auto;
214
  display: flex;
215
  flex-direction: column;
216
- gap: var(--padding-l);
217
  }
218
  .header {
219
  text-align: left;
@@ -231,29 +234,47 @@ TEMPLATE = """
231
  color: var(--text-secondary-color);
232
  margin-top: 4px;
233
  }
234
- .bonus-card {
 
 
 
 
 
235
  background: linear-gradient(145deg, #2a2a2a, #1c1c1c);
236
  border-radius: calc(var(--border-radius) + 8px);
237
  padding: var(--padding-l);
238
  text-align: center;
239
- box-shadow: var(--shadow-glow);
240
- border: 1px solid rgba(255, 193, 7, 0.2);
241
  position: relative;
242
  overflow: hidden;
243
  }
244
- .bonus-label {
 
 
 
 
 
 
 
 
245
  font-size: 1.1em;
246
  font-weight: 500;
247
  color: var(--text-secondary-color);
248
  margin-bottom: 12px;
249
  }
250
  .bonus-amount {
251
- font-size: 4em;
252
  font-weight: 800;
253
  color: var(--brand-yellow);
254
  letter-spacing: -2px;
255
  line-height: 1;
256
  }
 
 
 
 
 
 
 
257
  .client-id-card {
258
  background-color: var(--card-bg);
259
  border-radius: var(--border-radius);
@@ -318,13 +339,19 @@ TEMPLATE = """
318
  <body>
319
  <div class="container">
320
  <header class="header">
321
- <div class="logo">DRUZHBA<span>.</span></div>
322
  <p id="greeting" class="welcome-text">Добро пожаловать!</p>
323
  </header>
324
 
325
- <section class="bonus-card">
326
- <p class="bonus-label">Ваши бонусы</p>
327
- <p class="bonus-amount">{{ "%.2f"|format(user.bonuses|float) }}</p>
 
 
 
 
 
 
328
  </section>
329
 
330
  <section class="client-id-card">
@@ -334,17 +361,23 @@ TEMPLATE = """
334
 
335
  <section class="history-section">
336
  <h2 class="history-title">История операций</h2>
337
- {% if user.history %}
338
  <ul class="history-list">
339
- {% for item in user.history|sort(attribute='date', reverse=true) %}
340
  <li class="history-item">
341
  <div class="history-details">
342
  <span class="history-description">{{ item.description }}</span>
343
  <span class="history-date">{{ item.date_str }}</span>
344
  </div>
345
- <span class="history-amount {{ 'accrual' if item.type == 'accrual' else 'deduction' }}">
346
- {{ '+' if item.type == 'accrual' else '-' }}{{ "%.2f"|format(item.amount|float) }}
347
- </span>
 
 
 
 
 
 
348
  </li>
349
  {% endfor %}
350
  </ul>
@@ -440,7 +473,7 @@ ADMIN_TEMPLATE = """
440
  <head>
441
  <meta charset="UTF-8">
442
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
443
- <title>Druzhba Admin</title>
444
  <link rel="preconnect" href="https://fonts.googleapis.com">
445
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
446
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
@@ -463,6 +496,12 @@ ADMIN_TEMPLATE = """
463
  body { font-family: var(--font-family); background-color: var(--admin-bg); color: var(--admin-text); margin: 0; padding: var(--padding); line-height: 1.6; }
464
  .container { max-width: 1200px; margin: 0 auto; }
465
  h1 { text-align: center; color: var(--admin-secondary); margin-bottom: var(--padding); font-weight: 600; }
 
 
 
 
 
 
466
  .controls-bar { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; background: var(--admin-card-bg); padding: var(--padding); border-radius: var(--border-radius); box-shadow: 0 4px 15px var(--admin-shadow); border: 1px solid var(--admin-border); margin-bottom: var(--padding); }
467
  .controls-bar input[type="text"] { flex-grow: 1; padding: 12px 15px; font-size: 1.1em; border-radius: 8px; border: 1px solid var(--admin-border); box-sizing: border-box; min-width: 250px; }
468
  .btn { padding: 12px 20px; font-size: 1em; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; transition: background-color 0.2s ease; }
@@ -470,31 +509,34 @@ ADMIN_TEMPLATE = """
470
  .btn-primary:hover { background-color: var(--admin-primary-dark); }
471
  .btn-delete { background-color: var(--admin-danger); color: white; }
472
  .btn-delete:hover { background-color: #c82333; }
473
- .user-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: var(--padding); margin-top: var(--padding); }
474
  .user-card { background-color: var(--admin-card-bg); border-radius: var(--border-radius); padding: var(--padding); box-shadow: 0 4px 15px var(--admin-shadow); border: 1px solid var(--admin-border); display: flex; flex-direction: column; transition: transform 0.2s ease, box-shadow 0.2s ease; }
475
  .user-card:hover { transform: translateY(-5px); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08); }
476
  .user-info { display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; }
477
  .user-info img { width: 60px; height: 60px; border-radius: 50%; object-fit: cover; border: 3px solid var(--admin-border); background-color: #eee; }
478
  .user-details .name { font-weight: 600; font-size: 1.2em; }
479
  .user-details .username { color: var(--admin-secondary); font-size: 0.9em; }
480
- .user-bonuses { text-align: center; margin-bottom: 1rem; }
481
- .user-bonuses .label { font-size: 0.9em; color: var(--admin-secondary); }
482
- .user-bonuses .amount { font-size: 1.8em; font-weight: 700; color: var(--admin-primary-dark); }
 
483
  .user-actions { margin-top: auto; display: flex; flex-direction: column; gap: 0.5rem; }
484
  .btn-manage { display: block; width: 100%; padding: 10px; background-color: var(--admin-primary); color: #000; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; transition: background-color 0.2s; }
485
  .btn-manage:hover { background-color: var(--admin-primary-dark); }
486
  .no-users { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; }
487
  .modal { display: none; position: fixed; z-index: 1001; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.5); backdrop-filter: blur(5px); }
488
- .modal-content { background-color: var(--admin-bg); margin: 10% auto; padding: var(--padding); border: 1px solid var(--admin-border); width: 90%; max-width: 600px; border-radius: var(--border-radius); position: relative; box-shadow: 0 8px 30px rgba(0,0,0,0.15); }
489
  .modal-close { color: #aaa; position: absolute; top: 15px; right: 25px; font-size: 28px; font-weight: bold; cursor: pointer; }
490
  .modal-header { padding-bottom: 1rem; margin-bottom: 1.5rem; border-bottom: 1px solid var(--admin-border); }
491
  .modal-header h2 { margin: 0; font-size: 1.5rem; }
492
  .modal-header .username { font-size: 1rem; color: var(--admin-secondary); }
493
- .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: center; margin-bottom: 1.5rem; }
 
 
494
  .form-group { display: flex; flex-direction: column; }
495
  .form-group label { margin-bottom: 0.5rem; font-weight: 500; font-size: 0.9em; }
496
  .form-group input { padding: 10px; font-size: 1rem; border: 1px solid var(--admin-border); border-radius: 8px; width: 100%; box-sizing: border-box; }
497
- .calculation-summary { background: #f0f0f0; padding: 1rem; border-radius: 8px; margin-bottom: 1.5rem; }
498
  .summary-item { display: flex; justify-content: space-between; margin-bottom: 0.5rem; font-size: 0.95em; }
499
  .summary-item strong { font-weight: 600; }
500
  .history-container { margin-top: 1.5rem; }
@@ -504,8 +546,10 @@ ADMIN_TEMPLATE = """
504
  .history-item:last-child { border-bottom: none; }
505
  .history-item .desc { font-size: 0.9em; }
506
  .history-item .date { font-size: 0.8em; color: var(--admin-secondary); }
507
- .history-item .amount.accrual { color: var(--admin-success); font-weight: 600; }
508
- .history-item .amount.deduction { color: var(--admin-danger); font-weight: 600; }
 
 
509
  .modal-footer { margin-top: 1.5rem; display: flex; justify-content: flex-end; align-items: center; gap: 1rem;}
510
  .modal-footer button { padding: 12px 25px; font-size: 1.1em; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; }
511
  .btn-submit { background-color: var(--admin-success); color: white; }
@@ -514,7 +558,26 @@ ADMIN_TEMPLATE = """
514
  </head>
515
  <body>
516
  <div class="container">
517
- <h1>Панель администратора Druzhba</h1>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
518
  <div class="controls-bar">
519
  <input type="text" id="searchInput" onkeyup="searchUsers()" placeholder="Поиск по имени, ID, username, номеру...">
520
  <button class="btn btn-primary" onclick="openAddClientModal()">Добавить клиента</button>
@@ -531,12 +594,18 @@ ADMIN_TEMPLATE = """
531
  <div class="username">@{{ user.username if user.username else user.phone_number }} | ID: {{ user.id }}</div>
532
  </div>
533
  </div>
534
- <div class="user-bonuses">
535
- <div class="label">Текущие бонусы</div>
536
- <div class="amount">{{ "%.2f"|format(user.bonuses|float) }}</div>
 
 
 
 
 
 
537
  </div>
538
  <div class="user-actions">
539
- <button class="btn-manage" onclick='openTransactionModal({{ user|tojson }})'>Управление бонусами</button>
540
  {% if user.telegram_id == None %}
541
  <button class="btn btn-delete" onclick='deleteClient("{{ user.id }}")'>Удалить клиента</button>
542
  {% endif %}
@@ -557,25 +626,51 @@ ADMIN_TEMPLATE = """
557
  <div id="modalUserUsername" class="username"></div>
558
  </div>
559
  <input type="hidden" id="modalUserId">
560
- <div class="form-row">
561
- <div class="form-group">
562
- <label for="purchaseAmount">Сумма покупки</label>
563
- <input type="number" id="purchaseAmount" placeholder="Например, 1500" oninput="calculateBonuses()">
 
 
 
 
 
 
 
 
564
  </div>
565
- <div class="form-group">
566
- <label for="deductAmount">Списать бонусов</label>
567
- <input type="number" id="deductAmount" placeholder="Например, 100" oninput="calculateBonuses()">
 
 
 
568
  </div>
569
  </div>
570
- <div class="calculation-summary">
571
- <div class="summary-item"><span>Текущий баланс:</span> <strong id="summaryCurrentBalance">0.00</strong></div>
572
- <div class="summary-item"><span>Будет начислено (2%):</span> <strong id="summaryAccrual">+0.00</strong></div>
573
- <div class="summary-item"><span>Будет списано:</span> <strong id="summaryDeduction">-0.00</strong></div>
574
- <hr>
575
- <div class="summary-item"><strong>Итоговый баланс:</strong> <strong id="summaryFinalBalance">0.00</strong></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
576
  </div>
 
577
  <div class="history-container">
578
- <h3>История операций</h3>
579
  <ul id="modalHistoryList" class="history-list"></ul>
580
  </div>
581
  <div class="modal-footer">
@@ -631,23 +726,37 @@ ADMIN_TEMPLATE = """
631
  document.getElementById('modalUserUsername').textContent = `@${userData.username || userData.phone_number} | ID: ${userData.id}`;
632
  document.getElementById('purchaseAmount').value = '';
633
  document.getElementById('deductAmount').value = '';
 
 
634
  document.getElementById('modalStatus').textContent = '';
635
 
636
  const historyList = document.getElementById('modalHistoryList');
637
  historyList.innerHTML = '';
638
- if (userData.history && userData.history.length > 0) {
639
- const sortedHistory = [...userData.history].sort((a, b) => new Date(b.date) - new Date(a.date));
640
- sortedHistory.forEach(item => {
 
 
 
 
641
  const li = document.createElement('li');
642
  li.className = 'history-item';
643
- const sign = item.type === 'accrual' ? '+' : '-';
644
- const amountClass = item.type === 'accrual' ? 'accrual' : 'deduction';
 
 
 
 
 
 
 
 
645
  li.innerHTML = `
646
  <div>
647
  <div class="desc">${item.description}</div>
648
  <div class="date">${item.date_str}</div>
649
  </div>
650
- <div class="amount ${amountClass}">${sign}${parseFloat(item.amount).toFixed(2)}</div>
651
  `;
652
  historyList.appendChild(li);
653
  });
@@ -655,7 +764,7 @@ ADMIN_TEMPLATE = """
655
  historyList.innerHTML = '<li style="text-align:center; padding: 1rem; color: var(--admin-secondary);">Нет истории</li>';
656
  }
657
 
658
- calculateBonuses();
659
  transactionModal.style.display = 'block';
660
  }
661
 
@@ -673,8 +782,9 @@ ADMIN_TEMPLATE = """
673
  }
674
  }
675
 
676
- function calculateBonuses() {
677
  if (!currentUserData) return;
 
678
  const currentBalance = parseFloat(currentUserData.bonuses) || 0;
679
  const purchaseAmount = parseFloat(document.getElementById('purchaseAmount').value) || 0;
680
  const deductAmount = parseFloat(document.getElementById('deductAmount').value) || 0;
@@ -682,27 +792,45 @@ ADMIN_TEMPLATE = """
682
  let finalDeductAmount = deductAmount;
683
  if (deductAmount > currentBalance) {
684
  finalDeductAmount = currentBalance;
685
- document.getElementById('deductAmount').value = finalDeductAmount.toFixed(2);
686
  }
687
  const finalBalance = currentBalance + accrualAmount - finalDeductAmount;
688
  document.getElementById('summaryCurrentBalance').textContent = currentBalance.toFixed(2);
689
  document.getElementById('summaryAccrual').textContent = `+${accrualAmount.toFixed(2)}`;
690
  document.getElementById('summaryDeduction').textContent = `-${finalDeductAmount.toFixed(2)}`;
691
  document.getElementById('summaryFinalBalance').textContent = finalBalance.toFixed(2);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
692
  }
693
 
694
  async function submitTransaction() {
695
  const statusEl = document.getElementById('modalStatus');
696
  statusEl.style.color = 'var(--admin-secondary)';
697
  statusEl.textContent = 'Обработка...';
 
698
  const payload = {
699
  user_id: document.getElementById('modalUserId').value,
700
  purchase_amount: parseFloat(document.getElementById('purchaseAmount').value) || 0,
701
  deduct_amount: parseFloat(document.getElementById('deductAmount').value) || 0,
 
 
702
  };
703
- if (payload.purchase_amount <= 0 && payload.deduct_amount <= 0) {
 
704
  statusEl.style.color = 'var(--admin-danger)';
705
- statusEl.textContent = 'Введите сумму покупки или сумму для списания.';
706
  return;
707
  }
708
  try {
@@ -805,8 +933,30 @@ def index():
805
  if user_id_str and user_id_str in current_data:
806
  user_data = current_data[user_id_str]
807
  user_data['id'] = user_id_str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
808
  else:
809
- user_data = { "id": "N/A", "bonuses": 0, "history": [] }
 
 
 
 
 
 
 
810
 
811
  return render_template_string(TEMPLATE, user=user_data)
812
 
@@ -868,7 +1018,9 @@ def verify_data():
868
  'visited_at': now.timestamp(),
869
  'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
870
  'bonuses': 0,
871
- 'history': []
 
 
872
  }
873
  user_id_to_save = new_user_id
874
 
@@ -892,7 +1044,20 @@ def admin_panel():
892
  for user_id, user_data in current_data.items():
893
  user_data['id'] = user_id
894
  users_list.append(user_data)
895
- return render_template_string(ADMIN_TEMPLATE, users=users_list)
 
 
 
 
 
 
 
 
 
 
 
 
 
896
 
897
  @app.route('/admin/add_client', methods=['POST'])
898
  def add_client():
@@ -926,7 +1091,9 @@ def add_client():
926
  'visited_at': now.timestamp(),
927
  'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
928
  'bonuses': 0,
929
- 'history': []
 
 
930
  }
931
 
932
  save_visitor_data({new_id: new_client})
@@ -945,6 +1112,8 @@ def add_transaction():
945
  user_id = data.get('user_id')
946
  purchase_amount = float(data.get('purchase_amount', 0))
947
  deduct_amount = float(data.get('deduct_amount', 0))
 
 
948
 
949
  if not user_id:
950
  return jsonify({"status": "error", "message": "User ID is required"}), 400
@@ -957,39 +1126,58 @@ def add_transaction():
957
 
958
  user = all_data[user_id_str]
959
  now = datetime.now()
 
960
  now_str = now.strftime('%Y-%m-%d %H:%M:%S')
961
 
962
- accrual_amount = purchase_amount * 0.02
963
-
964
  if deduct_amount > user.get('bonuses', 0):
965
- return jsonify({"status": "error", "message": "Not enough bonuses to deduct"}), 400
 
 
 
966
 
 
 
967
  user['bonuses'] = user.get('bonuses', 0) + accrual_amount - deduct_amount
968
-
969
  if 'history' not in user or not isinstance(user['history'], list):
970
  user['history'] = []
971
 
972
  if accrual_amount > 0:
973
  user['history'].append({
974
- "type": "accrual",
975
- "amount": accrual_amount,
976
  "description": f"Начисление с покупки {purchase_amount}",
977
- "date": now.isoformat(),
978
- "date_str": now_str
979
  })
980
-
981
  if deduct_amount > 0:
982
  user['history'].append({
983
- "type": "deduction",
984
- "amount": deduct_amount,
985
  "description": "Списание бонусов",
986
- "date": now.isoformat(),
987
- "date_str": now_str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
988
  })
989
 
990
  save_visitor_data({user_id_str: user})
991
 
992
- return jsonify({"status": "ok", "message": "Transaction successful", "new_balance": user['bonuses']}), 200
 
 
 
993
 
994
  except Exception as e:
995
  logging.exception("Error in /admin/add_transaction endpoint")
@@ -1033,7 +1221,7 @@ def delete_client():
1033
  return jsonify({"status": "error", "message": str(e)}), 500
1034
 
1035
  if __name__ == '__main__':
1036
- print("--- DRUZHBA BONUS SYSTEM SERVER ---")
1037
  print(f"Server starting on http://{HOST}:{PORT}")
1038
  if not HF_TOKEN_READ or not HF_TOKEN_WRITE:
1039
  print("WARNING: Hugging Face token(s) not set. Backup/restore functionality will be limited.")
 
177
  <head>
178
  <meta charset="UTF-8">
179
  <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no, user-scalable=no, viewport-fit=cover">
180
+ <title>Bonus</title>
181
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
182
  <link rel="preconnect" href="https://fonts.googleapis.com">
183
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
 
186
  :root {
187
  --brand-yellow: #FFC107;
188
  --brand-black: #101010;
189
+ --brand-red: #F44336;
190
  --card-bg: #1c1c1e;
191
  --text-color: #ffffff;
192
  --text-secondary-color: #a0a0a0;
 
196
  --font-family: 'Manrope', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
197
  --shadow-color: rgba(255, 193, 7, 0.15);
198
  --shadow-glow: 0 0 35px var(--shadow-color);
199
+ --shadow-color-red: rgba(244, 67, 54, 0.15);
200
+ --shadow-glow-red: 0 0 35px var(--shadow-color-red);
201
  }
202
  * { box-sizing: border-box; margin: 0; padding: 0; }
203
  html, body {
 
216
  margin: 0 auto;
217
  display: flex;
218
  flex-direction: column;
219
+ gap: var(--padding-m);
220
  }
221
  .header {
222
  text-align: left;
 
234
  color: var(--text-secondary-color);
235
  margin-top: 4px;
236
  }
237
+ .card-grid {
238
+ display: grid;
239
+ grid-template-columns: 1fr 1fr;
240
+ gap: var(--padding-m);
241
+ }
242
+ .bonus-card, .debt-card {
243
  background: linear-gradient(145deg, #2a2a2a, #1c1c1c);
244
  border-radius: calc(var(--border-radius) + 8px);
245
  padding: var(--padding-l);
246
  text-align: center;
 
 
247
  position: relative;
248
  overflow: hidden;
249
  }
250
+ .bonus-card {
251
+ box-shadow: var(--shadow-glow);
252
+ border: 1px solid rgba(255, 193, 7, 0.2);
253
+ }
254
+ .debt-card {
255
+ box-shadow: var(--shadow-glow-red);
256
+ border: 1px solid rgba(244, 67, 54, 0.2);
257
+ }
258
+ .card-label {
259
  font-size: 1.1em;
260
  font-weight: 500;
261
  color: var(--text-secondary-color);
262
  margin-bottom: 12px;
263
  }
264
  .bonus-amount {
265
+ font-size: 3em;
266
  font-weight: 800;
267
  color: var(--brand-yellow);
268
  letter-spacing: -2px;
269
  line-height: 1;
270
  }
271
+ .debt-amount {
272
+ font-size: 3em;
273
+ font-weight: 800;
274
+ color: var(--brand-red);
275
+ letter-spacing: -2px;
276
+ line-height: 1;
277
+ }
278
  .client-id-card {
279
  background-color: var(--card-bg);
280
  border-radius: var(--border-radius);
 
339
  <body>
340
  <div class="container">
341
  <header class="header">
342
+ <div class="logo">BONUS<span>.</span></div>
343
  <p id="greeting" class="welcome-text">Добро пожаловать!</p>
344
  </header>
345
 
346
+ <section class="card-grid">
347
+ <div class="bonus-card">
348
+ <p class="card-label">Ваши бонусы</p>
349
+ <p class="bonus-amount">{{ "%.2f"|format(user.bonuses|float) }}</p>
350
+ </div>
351
+ <div class="debt-card">
352
+ <p class="card-label">Ваш долг</p>
353
+ <p class="debt-amount">{{ "%.2f"|format(user.debts|float) }}</p>
354
+ </div>
355
  </section>
356
 
357
  <section class="client-id-card">
 
361
 
362
  <section class="history-section">
363
  <h2 class="history-title">История операций</h2>
364
+ {% if user.combined_history %}
365
  <ul class="history-list">
366
+ {% for item in user.combined_history %}
367
  <li class="history-item">
368
  <div class="history-details">
369
  <span class="history-description">{{ item.description }}</span>
370
  <span class="history-date">{{ item.date_str }}</span>
371
  </div>
372
+ {% if item.transaction_type == 'bonus' %}
373
+ <span class="history-amount {{ 'accrual' if item.type == 'accrual' else 'deduction' }}">
374
+ {{ '+' if item.type == 'accrual' else '-' }}{{ "%.2f"|format(item.amount|float) }}
375
+ </span>
376
+ {% elif item.transaction_type == 'debt' %}
377
+ <span class="history-amount {{ 'deduction' if item.type == 'accrual' else 'accrual' }}">
378
+ {{ '+' if item.type == 'accrual' else '-' }}{{ "%.2f"|format(item.amount|float) }}
379
+ </span>
380
+ {% endif %}
381
  </li>
382
  {% endfor %}
383
  </ul>
 
473
  <head>
474
  <meta charset="UTF-8">
475
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
476
+ <title>Bonus Admin</title>
477
  <link rel="preconnect" href="https://fonts.googleapis.com">
478
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
479
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
 
496
  body { font-family: var(--font-family); background-color: var(--admin-bg); color: var(--admin-text); margin: 0; padding: var(--padding); line-height: 1.6; }
497
  .container { max-width: 1200px; margin: 0 auto; }
498
  h1 { text-align: center; color: var(--admin-secondary); margin-bottom: var(--padding); font-weight: 600; }
499
+ .summary-bar { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: var(--padding); margin-bottom: var(--padding); }
500
+ .summary-card { background: var(--admin-card-bg); padding: var(--padding); border-radius: var(--border-radius); box-shadow: 0 4px 15px var(--admin-shadow); border: 1px solid var(--admin-border); text-align: center; }
501
+ .summary-card .value { font-size: 2em; font-weight: 700; }
502
+ .summary-card .label { font-size: 0.9em; color: var(--admin-secondary); margin-top: 0.5rem; }
503
+ .summary-card .value.bonus { color: var(--admin-primary-dark); }
504
+ .summary-card .value.debt { color: var(--admin-danger); }
505
  .controls-bar { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; background: var(--admin-card-bg); padding: var(--padding); border-radius: var(--border-radius); box-shadow: 0 4px 15px var(--admin-shadow); border: 1px solid var(--admin-border); margin-bottom: var(--padding); }
506
  .controls-bar input[type="text"] { flex-grow: 1; padding: 12px 15px; font-size: 1.1em; border-radius: 8px; border: 1px solid var(--admin-border); box-sizing: border-box; min-width: 250px; }
507
  .btn { padding: 12px 20px; font-size: 1em; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; transition: background-color 0.2s ease; }
 
509
  .btn-primary:hover { background-color: var(--admin-primary-dark); }
510
  .btn-delete { background-color: var(--admin-danger); color: white; }
511
  .btn-delete:hover { background-color: #c82333; }
512
+ .user-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: var(--padding); margin-top: var(--padding); }
513
  .user-card { background-color: var(--admin-card-bg); border-radius: var(--border-radius); padding: var(--padding); box-shadow: 0 4px 15px var(--admin-shadow); border: 1px solid var(--admin-border); display: flex; flex-direction: column; transition: transform 0.2s ease, box-shadow 0.2s ease; }
514
  .user-card:hover { transform: translateY(-5px); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08); }
515
  .user-info { display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; }
516
  .user-info img { width: 60px; height: 60px; border-radius: 50%; object-fit: cover; border: 3px solid var(--admin-border); background-color: #eee; }
517
  .user-details .name { font-weight: 600; font-size: 1.2em; }
518
  .user-details .username { color: var(--admin-secondary); font-size: 0.9em; }
519
+ .user-balances { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; text-align: center; margin-bottom: 1rem; }
520
+ .user-balances .label { font-size: 0.9em; color: var(--admin-secondary); }
521
+ .user-balances .amount.bonus { font-size: 1.8em; font-weight: 700; color: var(--admin-primary-dark); }
522
+ .user-balances .amount.debt { font-size: 1.8em; font-weight: 700; color: var(--admin-danger); }
523
  .user-actions { margin-top: auto; display: flex; flex-direction: column; gap: 0.5rem; }
524
  .btn-manage { display: block; width: 100%; padding: 10px; background-color: var(--admin-primary); color: #000; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; transition: background-color 0.2s; }
525
  .btn-manage:hover { background-color: var(--admin-primary-dark); }
526
  .no-users { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; }
527
  .modal { display: none; position: fixed; z-index: 1001; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.5); backdrop-filter: blur(5px); }
528
+ .modal-content { background-color: var(--admin-bg); margin: 5% auto; padding: var(--padding); border: 1px solid var(--admin-border); width: 90%; max-width: 700px; border-radius: var(--border-radius); position: relative; box-shadow: 0 8px 30px rgba(0,0,0,0.15); }
529
  .modal-close { color: #aaa; position: absolute; top: 15px; right: 25px; font-size: 28px; font-weight: bold; cursor: pointer; }
530
  .modal-header { padding-bottom: 1rem; margin-bottom: 1.5rem; border-bottom: 1px solid var(--admin-border); }
531
  .modal-header h2 { margin: 0; font-size: 1.5rem; }
532
  .modal-header .username { font-size: 1rem; color: var(--admin-secondary); }
533
+ .form-section { border: 1px solid var(--admin-border); border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem; }
534
+ .form-section h3 { margin-top: 0; margin-bottom: 1rem; font-size: 1.1em; }
535
+ .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: flex-end; }
536
  .form-group { display: flex; flex-direction: column; }
537
  .form-group label { margin-bottom: 0.5rem; font-weight: 500; font-size: 0.9em; }
538
  .form-group input { padding: 10px; font-size: 1rem; border: 1px solid var(--admin-border); border-radius: 8px; width: 100%; box-sizing: border-box; }
539
+ .calculation-summary { background: #f0f0f0; padding: 1rem; border-radius: 8px; margin-top: 1rem; }
540
  .summary-item { display: flex; justify-content: space-between; margin-bottom: 0.5rem; font-size: 0.95em; }
541
  .summary-item strong { font-weight: 600; }
542
  .history-container { margin-top: 1.5rem; }
 
546
  .history-item:last-child { border-bottom: none; }
547
  .history-item .desc { font-size: 0.9em; }
548
  .history-item .date { font-size: 0.8em; color: var(--admin-secondary); }
549
+ .history-item .amount.bonus-accrual { color: var(--admin-success); font-weight: 600; }
550
+ .history-item .amount.bonus-deduction { color: var(--admin-danger); font-weight: 600; }
551
+ .history-item .amount.debt-accrual { color: var(--admin-danger); font-weight: 600; }
552
+ .history-item .amount.debt-payment { color: var(--admin-success); font-weight: 600; }
553
  .modal-footer { margin-top: 1.5rem; display: flex; justify-content: flex-end; align-items: center; gap: 1rem;}
554
  .modal-footer button { padding: 12px 25px; font-size: 1.1em; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; }
555
  .btn-submit { background-color: var(--admin-success); color: white; }
 
558
  </head>
559
  <body>
560
  <div class="container">
561
+ <h1>Панель администратора Bonus</h1>
562
+ <div class="summary-bar">
563
+ <div class="summary-card">
564
+ <div class="value">{{ summary.total_users }}</div>
565
+ <div class="label">Всего клиентов</div>
566
+ </div>
567
+ <div class="summary-card">
568
+ <div class="value bonus">{{ "%.2f"|format(summary.total_bonuses|float) }}</div>
569
+ <div class="label">Всего бонусов</div>
570
+ </div>
571
+ <div class="summary-card">
572
+ <div class="value debt">{{ "%.2f"|format(summary.total_debts|float) }}</div>
573
+ <div class="label">Всего долгов</div>
574
+ </div>
575
+ <div class="summary-card">
576
+ <div class="value debt">{{ summary.users_with_debt }}</div>
577
+ <div class="label">Клиенты с долгом</div>
578
+ </div>
579
+ </div>
580
+
581
  <div class="controls-bar">
582
  <input type="text" id="searchInput" onkeyup="searchUsers()" placeholder="Поиск по имени, ID, username, номеру...">
583
  <button class="btn btn-primary" onclick="openAddClientModal()">Добавить клиента</button>
 
594
  <div class="username">@{{ user.username if user.username else user.phone_number }} | ID: {{ user.id }}</div>
595
  </div>
596
  </div>
597
+ <div class="user-balances">
598
+ <div>
599
+ <div class="label">Бонусы</div>
600
+ <div class="amount bonus">{{ "%.2f"|format(user.bonuses|float) }}</div>
601
+ </div>
602
+ <div>
603
+ <div class="label">Долг</div>
604
+ <div class="amount debt">{{ "%.2f"|format(user.debts|float if user.debts else 0) }}</div>
605
+ </div>
606
  </div>
607
  <div class="user-actions">
608
+ <button class="btn-manage" onclick='openTransactionModal({{ user|tojson }})'>Управление счетом</button>
609
  {% if user.telegram_id == None %}
610
  <button class="btn btn-delete" onclick='deleteClient("{{ user.id }}")'>Удалить клиента</button>
611
  {% endif %}
 
626
  <div id="modalUserUsername" class="username"></div>
627
  </div>
628
  <input type="hidden" id="modalUserId">
629
+
630
+ <div class="form-section">
631
+ <h3>Бонусы</h3>
632
+ <div class="form-row">
633
+ <div class="form-group">
634
+ <label for="purchaseAmount">Сумма покупки (для начисления)</label>
635
+ <input type="number" id="purchaseAmount" placeholder="1500" oninput="updateCalculations()">
636
+ </div>
637
+ <div class="form-group">
638
+ <label for="deductAmount">Списать бонусов</label>
639
+ <input type="number" id="deductAmount" placeholder="100" oninput="updateCalculations()">
640
+ </div>
641
  </div>
642
+ <div class="calculation-summary">
643
+ <div class="summary-item"><span>Текущий баланс:</span> <strong id="summaryCurrentBalance">0.00</strong></div>
644
+ <div class="summary-item"><span>Будет начислено (2%):</span> <strong id="summaryAccrual">+0.00</strong></div>
645
+ <div class="summary-item"><span>Будет списано:</span> <strong id="summaryDeduction">-0.00</strong></div>
646
+ <hr>
647
+ <div class="summary-item"><strong>Итоговый баланс бонусов:</strong> <strong id="summaryFinalBalance">0.00</strong></div>
648
  </div>
649
  </div>
650
+
651
+ <div class="form-section">
652
+ <h3>Долги</h3>
653
+ <div class="form-row">
654
+ <div class="form-group">
655
+ <label for="addDebtAmount">Добавить долг</label>
656
+ <input type="number" id="addDebtAmount" placeholder="500" oninput="updateCalculations()">
657
+ </div>
658
+ <div class="form-group">
659
+ <label for="repayDebtAmount">Погасить долг</label>
660
+ <input type="number" id="repayDebtAmount" placeholder="200" oninput="updateCalculations()">
661
+ </div>
662
+ </div>
663
+ <div class="calculation-summary">
664
+ <div class="summary-item"><span>Текущий долг:</span> <strong id="summaryCurrentDebt">0.00</strong></div>
665
+ <div class="summary-item"><span>Будет добавлено:</span> <strong id="summaryAddDebt">+0.00</strong></div>
666
+ <div class="summary-item"><span>Будет погашено:</span> <strong id="summaryRepayDebt">-0.00</strong></div>
667
+ <hr>
668
+ <div class="summary-item"><strong>Итоговый долг:</strong> <strong id="summaryFinalDebt">0.00</strong></div>
669
+ </div>
670
  </div>
671
+
672
  <div class="history-container">
673
+ <h3>Общая история операций</h3>
674
  <ul id="modalHistoryList" class="history-list"></ul>
675
  </div>
676
  <div class="modal-footer">
 
726
  document.getElementById('modalUserUsername').textContent = `@${userData.username || userData.phone_number} | ID: ${userData.id}`;
727
  document.getElementById('purchaseAmount').value = '';
728
  document.getElementById('deductAmount').value = '';
729
+ document.getElementById('addDebtAmount').value = '';
730
+ document.getElementById('repayDebtAmount').value = '';
731
  document.getElementById('modalStatus').textContent = '';
732
 
733
  const historyList = document.getElementById('modalHistoryList');
734
  historyList.innerHTML = '';
735
+
736
+ const bonusHistory = (userData.history || []).map(h => ({...h, transaction_type: 'bonus'}));
737
+ const debtHistory = (userData.debt_history || []).map(h => ({...h, transaction_type: 'debt'}));
738
+ const combinedHistory = [...bonusHistory, ...debtHistory].sort((a, b) => new Date(b.date) - new Date(a.date));
739
+
740
+ if (combinedHistory.length > 0) {
741
+ combinedHistory.forEach(item => {
742
  const li = document.createElement('li');
743
  li.className = 'history-item';
744
+ let sign, amountClass, amountText;
745
+ if (item.transaction_type === 'bonus') {
746
+ sign = item.type === 'accrual' ? '+' : '-';
747
+ amountClass = item.type === 'accrual' ? 'bonus-accrual' : 'bonus-deduction';
748
+ amountText = `${sign}${parseFloat(item.amount).toFixed(2)} (бонус)`;
749
+ } else { // debt
750
+ sign = item.type === 'accrual' ? '+' : '-';
751
+ amountClass = item.type === 'accrual' ? 'debt-accrual' : 'debt-payment';
752
+ amountText = `${sign}${parseFloat(item.amount).toFixed(2)} (долг)`;
753
+ }
754
  li.innerHTML = `
755
  <div>
756
  <div class="desc">${item.description}</div>
757
  <div class="date">${item.date_str}</div>
758
  </div>
759
+ <div class="amount ${amountClass}">${amountText}</div>
760
  `;
761
  historyList.appendChild(li);
762
  });
 
764
  historyList.innerHTML = '<li style="text-align:center; padding: 1rem; color: var(--admin-secondary);">Нет истории</li>';
765
  }
766
 
767
+ updateCalculations();
768
  transactionModal.style.display = 'block';
769
  }
770
 
 
782
  }
783
  }
784
 
785
+ function updateCalculations() {
786
  if (!currentUserData) return;
787
+
788
  const currentBalance = parseFloat(currentUserData.bonuses) || 0;
789
  const purchaseAmount = parseFloat(document.getElementById('purchaseAmount').value) || 0;
790
  const deductAmount = parseFloat(document.getElementById('deductAmount').value) || 0;
 
792
  let finalDeductAmount = deductAmount;
793
  if (deductAmount > currentBalance) {
794
  finalDeductAmount = currentBalance;
795
+ document.getElementById('deductAmount').value = finalDeductAmount > 0 ? finalDeductAmount.toFixed(2) : '';
796
  }
797
  const finalBalance = currentBalance + accrualAmount - finalDeductAmount;
798
  document.getElementById('summaryCurrentBalance').textContent = currentBalance.toFixed(2);
799
  document.getElementById('summaryAccrual').textContent = `+${accrualAmount.toFixed(2)}`;
800
  document.getElementById('summaryDeduction').textContent = `-${finalDeductAmount.toFixed(2)}`;
801
  document.getElementById('summaryFinalBalance').textContent = finalBalance.toFixed(2);
802
+
803
+ const currentDebt = parseFloat(currentUserData.debts) || 0;
804
+ const addDebtAmount = parseFloat(document.getElementById('addDebtAmount').value) || 0;
805
+ const repayDebtAmount = parseFloat(document.getElementById('repayDebtAmount').value) || 0;
806
+ let finalRepayAmount = repayDebtAmount;
807
+ if (repayDebtAmount > currentDebt) {
808
+ finalRepayAmount = currentDebt;
809
+ document.getElementById('repayDebtAmount').value = finalRepayAmount > 0 ? finalRepayAmount.toFixed(2) : '';
810
+ }
811
+ const finalDebt = currentDebt + addDebtAmount - finalRepayAmount;
812
+ document.getElementById('summaryCurrentDebt').textContent = currentDebt.toFixed(2);
813
+ document.getElementById('summaryAddDebt').textContent = `+${addDebtAmount.toFixed(2)}`;
814
+ document.getElementById('summaryRepayDebt').textContent = `-${finalRepayAmount.toFixed(2)}`;
815
+ document.getElementById('summaryFinalDebt').textContent = finalDebt.toFixed(2);
816
  }
817
 
818
  async function submitTransaction() {
819
  const statusEl = document.getElementById('modalStatus');
820
  statusEl.style.color = 'var(--admin-secondary)';
821
  statusEl.textContent = 'Обработка...';
822
+
823
  const payload = {
824
  user_id: document.getElementById('modalUserId').value,
825
  purchase_amount: parseFloat(document.getElementById('purchaseAmount').value) || 0,
826
  deduct_amount: parseFloat(document.getElementById('deductAmount').value) || 0,
827
+ add_debt_amount: parseFloat(document.getElementById('addDebtAmount').value) || 0,
828
+ repay_debt_amount: parseFloat(document.getElementById('repayDebtAmount').value) || 0,
829
  };
830
+
831
+ if (payload.purchase_amount <= 0 && payload.deduct_amount <= 0 && payload.add_debt_amount <= 0 && payload.repay_debt_amount <= 0) {
832
  statusEl.style.color = 'var(--admin-danger)';
833
+ statusEl.textContent = 'Введите сумму для любой из операций.';
834
  return;
835
  }
836
  try {
 
933
  if user_id_str and user_id_str in current_data:
934
  user_data = current_data[user_id_str]
935
  user_data['id'] = user_id_str
936
+
937
+ bonus_history = user_data.get('history', [])
938
+ for item in bonus_history:
939
+ item['transaction_type'] = 'bonus'
940
+
941
+ debt_history = user_data.get('debt_history', [])
942
+ for item in debt_history:
943
+ item['transaction_type'] = 'debt'
944
+
945
+ combined_history = sorted(
946
+ bonus_history + debt_history,
947
+ key=lambda x: x['date'],
948
+ reverse=True
949
+ )
950
+ user_data['combined_history'] = combined_history
951
  else:
952
+ user_data = {
953
+ "id": "N/A",
954
+ "bonuses": 0,
955
+ "debts": 0,
956
+ "history": [],
957
+ "debt_history": [],
958
+ "combined_history": []
959
+ }
960
 
961
  return render_template_string(TEMPLATE, user=user_data)
962
 
 
1018
  'visited_at': now.timestamp(),
1019
  'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
1020
  'bonuses': 0,
1021
+ 'history': [],
1022
+ 'debts': 0,
1023
+ 'debt_history': []
1024
  }
1025
  user_id_to_save = new_user_id
1026
 
 
1044
  for user_id, user_data in current_data.items():
1045
  user_data['id'] = user_id
1046
  users_list.append(user_data)
1047
+
1048
+ total_users = len(users_list)
1049
+ total_bonuses = sum(u.get('bonuses', 0) for u in users_list)
1050
+ total_debts = sum(u.get('debts', 0) for u in users_list)
1051
+ users_with_debt = sum(1 for u in users_list if u.get('debts', 0) > 0)
1052
+
1053
+ summary_stats = {
1054
+ "total_users": total_users,
1055
+ "total_bonuses": total_bonuses,
1056
+ "total_debts": total_debts,
1057
+ "users_with_debt": users_with_debt
1058
+ }
1059
+
1060
+ return render_template_string(ADMIN_TEMPLATE, users=users_list, summary=summary_stats)
1061
 
1062
  @app.route('/admin/add_client', methods=['POST'])
1063
  def add_client():
 
1091
  'visited_at': now.timestamp(),
1092
  'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
1093
  'bonuses': 0,
1094
+ 'history': [],
1095
+ 'debts': 0,
1096
+ 'debt_history': []
1097
  }
1098
 
1099
  save_visitor_data({new_id: new_client})
 
1112
  user_id = data.get('user_id')
1113
  purchase_amount = float(data.get('purchase_amount', 0))
1114
  deduct_amount = float(data.get('deduct_amount', 0))
1115
+ add_debt_amount = float(data.get('add_debt_amount', 0))
1116
+ repay_debt_amount = float(data.get('repay_debt_amount', 0))
1117
 
1118
  if not user_id:
1119
  return jsonify({"status": "error", "message": "User ID is required"}), 400
 
1126
 
1127
  user = all_data[user_id_str]
1128
  now = datetime.now()
1129
+ now_iso = now.isoformat()
1130
  now_str = now.strftime('%Y-%m-%d %H:%M:%S')
1131
 
 
 
1132
  if deduct_amount > user.get('bonuses', 0):
1133
+ return jsonify({"status": "error", "message": "Недостаточно бонусов для списания"}), 400
1134
+
1135
+ if repay_debt_amount > user.get('debts', 0):
1136
+ return jsonify({"status": "error", "message": "Сумма погашения превышает текущий долг"}), 400
1137
 
1138
+ # Bonus operations
1139
+ accrual_amount = purchase_amount * 0.02
1140
  user['bonuses'] = user.get('bonuses', 0) + accrual_amount - deduct_amount
 
1141
  if 'history' not in user or not isinstance(user['history'], list):
1142
  user['history'] = []
1143
 
1144
  if accrual_amount > 0:
1145
  user['history'].append({
1146
+ "type": "accrual", "amount": accrual_amount,
 
1147
  "description": f"Начисление с покупки {purchase_amount}",
1148
+ "date": now_iso, "date_str": now_str
 
1149
  })
 
1150
  if deduct_amount > 0:
1151
  user['history'].append({
1152
+ "type": "deduction", "amount": deduct_amount,
 
1153
  "description": "Списание бонусов",
1154
+ "date": now_iso, "date_str": now_str
1155
+ })
1156
+
1157
+ # Debt operations
1158
+ user['debts'] = user.get('debts', 0) + add_debt_amount - repay_debt_amount
1159
+ if 'debt_history' not in user or not isinstance(user['debt_history'], list):
1160
+ user['debt_history'] = []
1161
+
1162
+ if add_debt_amount > 0:
1163
+ user['debt_history'].append({
1164
+ "type": "accrual", "amount": add_debt_amount,
1165
+ "description": "Добавление долга",
1166
+ "date": now_iso, "date_str": now_str
1167
+ })
1168
+ if repay_debt_amount > 0:
1169
+ user['debt_history'].append({
1170
+ "type": "payment", "amount": repay_debt_amount,
1171
+ "description": "Погашение долга",
1172
+ "date": now_iso, "date_str": now_str
1173
  })
1174
 
1175
  save_visitor_data({user_id_str: user})
1176
 
1177
+ return jsonify({
1178
+ "status": "ok", "message": "Transaction successful",
1179
+ "new_balance": user['bonuses'], "new_debt": user['debts']
1180
+ }), 200
1181
 
1182
  except Exception as e:
1183
  logging.exception("Error in /admin/add_transaction endpoint")
 
1221
  return jsonify({"status": "error", "message": str(e)}), 500
1222
 
1223
  if __name__ == '__main__':
1224
+ print("--- BONUS SYSTEM SERVER ---")
1225
  print(f"Server starting on http://{HOST}:{PORT}")
1226
  if not HF_TOKEN_READ or not HF_TOKEN_WRITE:
1227
  print("WARNING: Hugging Face token(s) not set. Backup/restore functionality will be limited.")