KyrosDev commited on
Commit
588c4fc
·
1 Parent(s): 471bcb5

新增用戶管理編輯功能和版本發布表單資料保存

Browse files

- 新增授權編輯功能支援修改用戶名稱和電子郵件
- 編輯按鈕位於授權詳情標題右側
- 編輯完成後自動返回授權詳情頁面
- 版本發布表單支援頁籤切換時保存資料
- 移除表單保存相關的控制台日誌輸出

app/api/license.py CHANGED
@@ -6,7 +6,7 @@ from fastapi import APIRouter, HTTPException, Request, Depends
6
  from fastapi.responses import JSONResponse
7
  from typing import List, Dict, Any
8
  from app.models.license import (
9
- LicenseActivation, LicenseValidation, LicenseCreate, LicenseExtend,
10
  ActivationResponse, ValidationResponse, APIResponse
11
  )
12
  from app.services.license_service import license_service
@@ -168,8 +168,8 @@ async def extend_license(license_id: str, extend_data: LicenseExtend):
168
  """延長授權天數 (管理界面使用)"""
169
  try:
170
  result = await license_service.extend_license(
171
- license_id,
172
- extend_data.extend_days,
173
  extend_data.reason
174
  )
175
  return APIResponse(
@@ -177,5 +177,18 @@ async def extend_license(license_id: str, extend_data: LicenseExtend):
177
  message=result["message"],
178
  data=result.get("data")
179
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  except Exception as e:
181
  raise HTTPException(status_code=500, detail=f"系統錯誤: {str(e)}")
 
6
  from fastapi.responses import JSONResponse
7
  from typing import List, Dict, Any
8
  from app.models.license import (
9
+ LicenseActivation, LicenseValidation, LicenseCreate, LicenseExtend, LicenseUpdate,
10
  ActivationResponse, ValidationResponse, APIResponse
11
  )
12
  from app.services.license_service import license_service
 
168
  """延長授權天數 (管理界面使用)"""
169
  try:
170
  result = await license_service.extend_license(
171
+ license_id,
172
+ extend_data.extend_days,
173
  extend_data.reason
174
  )
175
  return APIResponse(
 
177
  message=result["message"],
178
  data=result.get("data")
179
  )
180
+ except Exception as e:
181
+ raise HTTPException(status_code=500, detail=f"系統錯誤: {str(e)}")
182
+
183
+ @router.patch("/license/{license_id}")
184
+ async def update_license(license_id: str, update_data: LicenseUpdate):
185
+ """更新授權資訊 (管理界面使用)"""
186
+ try:
187
+ result = await license_service.update_license(license_id, update_data)
188
+ return APIResponse(
189
+ success=result["success"],
190
+ message=result["message"],
191
+ data=result.get("data")
192
+ )
193
  except Exception as e:
194
  raise HTTPException(status_code=500, detail=f"系統錯誤: {str(e)}")
app/models/license.py CHANGED
@@ -33,6 +33,11 @@ class LicenseExtend(BaseModel):
33
  extend_days: int
34
  reason: Optional[str] = None
35
 
 
 
 
 
 
36
  # === API 回應模型 ===
37
 
38
  class APIResponse(BaseModel):
 
33
  extend_days: int
34
  reason: Optional[str] = None
35
 
36
+ class LicenseUpdate(BaseModel):
37
+ """更新授權請求"""
38
+ user_name: Optional[str] = None
39
+ user_email: Optional[EmailStr] = None
40
+
41
  # === API 回應模型 ===
42
 
43
  class APIResponse(BaseModel):
app/services/license_service.py CHANGED
@@ -9,7 +9,7 @@ from datetime import datetime, timedelta, timezone
9
  from typing import Optional, List, Dict, Any
10
  from app.models.supabase_clients import supabase_clients
11
  from app.models.license import (
12
- LicenseActivation, LicenseValidation, LicenseCreate,
13
  ActivationResponse, ValidationResponse, License, UsageLog, LicenseStats
14
  )
15
 
@@ -422,19 +422,19 @@ class LicenseService:
422
  if not self.supabase:
423
  print("❌ extend_license: Supabase client not available")
424
  return {"success": False, "message": "資料庫連接失敗"}
425
-
426
  if extend_days <= 0:
427
  return {"success": False, "message": "延長天數必須大於 0"}
428
-
429
  # 取得目前授權資料
430
  result = self.supabase.table("licenses").select("*").eq("id", license_id).execute()
431
-
432
  if not result.data:
433
  return {"success": False, "message": "授權不存在"}
434
-
435
  license_data = result.data[0]
436
  current_expires_at = datetime.fromisoformat(license_data["expires_at"].replace('Z', '+00:00'))
437
-
438
  # 計算新的到期時間:從原到期日開始加天數(-8hr對應台灣時間)
439
  if current_expires_at < datetime.now(timezone.utc):
440
  # 如果已過期,從今天開始計算
@@ -447,20 +447,20 @@ class LicenseService:
447
  next_day = expire_date + timedelta(days=1)
448
  start_date = datetime.combine(next_day, datetime.min.time(), timezone.utc) - timedelta(hours=8)
449
  new_expires_at = start_date + timedelta(days=extend_days) - timedelta(seconds=1)
450
-
451
  # 更新授權
452
  update_data = {
453
  "expires_at": new_expires_at.isoformat(),
454
  "is_active": True # 延長時自動啟用授權
455
  }
456
-
457
  update_result = self.supabase.table("licenses").update(update_data).eq("id", license_id).execute()
458
-
459
  if update_result.data:
460
  # 記錄延長操作
461
  extend_reason = reason or f"延長 {extend_days} 天"
462
  await self._log_usage(license_id, "extend", None, None, extend_reason)
463
-
464
  return {
465
  "success": True,
466
  "message": f"授權已延長 {extend_days} 天",
@@ -472,10 +472,53 @@ class LicenseService:
472
  }
473
  else:
474
  return {"success": False, "message": "延長失敗"}
475
-
476
  except Exception as e:
477
  print(f"❌ 延長授權錯誤: {e}")
478
  return {"success": False, "message": f"延長失敗: {str(e)}"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
479
 
480
  async def _log_usage(self, license_id: Optional[str], action: str, ip_address: Optional[str] = None,
481
  hardware_info: Optional[str] = None, machine_name: Optional[str] = None,
 
9
  from typing import Optional, List, Dict, Any
10
  from app.models.supabase_clients import supabase_clients
11
  from app.models.license import (
12
+ LicenseActivation, LicenseValidation, LicenseCreate, LicenseUpdate,
13
  ActivationResponse, ValidationResponse, License, UsageLog, LicenseStats
14
  )
15
 
 
422
  if not self.supabase:
423
  print("❌ extend_license: Supabase client not available")
424
  return {"success": False, "message": "資料庫連接失敗"}
425
+
426
  if extend_days <= 0:
427
  return {"success": False, "message": "延長天數必須大於 0"}
428
+
429
  # 取得目前授權資料
430
  result = self.supabase.table("licenses").select("*").eq("id", license_id).execute()
431
+
432
  if not result.data:
433
  return {"success": False, "message": "授權不存在"}
434
+
435
  license_data = result.data[0]
436
  current_expires_at = datetime.fromisoformat(license_data["expires_at"].replace('Z', '+00:00'))
437
+
438
  # 計算新的到期時間:從原到期日開始加天數(-8hr對應台灣時間)
439
  if current_expires_at < datetime.now(timezone.utc):
440
  # 如果已過期,從今天開始計算
 
447
  next_day = expire_date + timedelta(days=1)
448
  start_date = datetime.combine(next_day, datetime.min.time(), timezone.utc) - timedelta(hours=8)
449
  new_expires_at = start_date + timedelta(days=extend_days) - timedelta(seconds=1)
450
+
451
  # 更新授權
452
  update_data = {
453
  "expires_at": new_expires_at.isoformat(),
454
  "is_active": True # 延長時自動啟用授權
455
  }
456
+
457
  update_result = self.supabase.table("licenses").update(update_data).eq("id", license_id).execute()
458
+
459
  if update_result.data:
460
  # 記錄延長操作
461
  extend_reason = reason or f"延長 {extend_days} 天"
462
  await self._log_usage(license_id, "extend", None, None, extend_reason)
463
+
464
  return {
465
  "success": True,
466
  "message": f"授權已延長 {extend_days} 天",
 
472
  }
473
  else:
474
  return {"success": False, "message": "延長失敗"}
475
+
476
  except Exception as e:
477
  print(f"❌ 延長授權錯誤: {e}")
478
  return {"success": False, "message": f"延長失敗: {str(e)}"}
479
+
480
+ async def update_license(self, license_id: str, update_data: LicenseUpdate) -> Dict[str, Any]:
481
+ """更新授權資訊"""
482
+ try:
483
+ if not self.supabase:
484
+ print("❌ update_license: Supabase client not available")
485
+ return {"success": False, "message": "資料庫連接失敗"}
486
+
487
+ # 檢查授權是否存在
488
+ result = self.supabase.table("licenses").select("*").eq("id", license_id).execute()
489
+
490
+ if not result.data:
491
+ return {"success": False, "message": "授權不存在"}
492
+
493
+ # 準備更新資料(只更新有提供的欄位)
494
+ updates = {}
495
+ if update_data.user_name is not None:
496
+ updates["user_name"] = update_data.user_name
497
+ if update_data.user_email is not None:
498
+ updates["user_email"] = update_data.user_email
499
+
500
+ if not updates:
501
+ return {"success": False, "message": "沒有要更新的資料"}
502
+
503
+ # 執行更新
504
+ update_result = self.supabase.table("licenses").update(updates).eq("id", license_id).execute()
505
+
506
+ if update_result.data:
507
+ # 記錄更新操作
508
+ update_fields = ", ".join(updates.keys())
509
+ await self._log_usage(license_id, "update", None, None, f"更新授權資訊: {update_fields}")
510
+
511
+ return {
512
+ "success": True,
513
+ "message": "授權資訊更新成功",
514
+ "data": update_result.data[0]
515
+ }
516
+ else:
517
+ return {"success": False, "message": "更新失敗"}
518
+
519
+ except Exception as e:
520
+ print(f"❌ 更新授權錯誤: {e}")
521
+ return {"success": False, "message": f"更新失敗: {str(e)}"}
522
 
523
  async def _log_usage(self, license_id: Optional[str], action: str, ip_address: Optional[str] = None,
524
  hardware_info: Optional[str] = None, machine_name: Optional[str] = None,
frontend/js/api.js CHANGED
@@ -311,7 +311,14 @@ class ApiClient {
311
  body: extendData
312
  });
313
  }
314
-
 
 
 
 
 
 
 
315
  // Stats endpoints
316
  async getLicenseStats() {
317
  if (this.isDevMode) {
 
311
  body: extendData
312
  });
313
  }
314
+
315
+ async updateLicense(licenseId, updateData) {
316
+ return this.request(`/api/license/${licenseId}`, {
317
+ method: 'PATCH',
318
+ body: updateData
319
+ });
320
+ }
321
+
322
  // Stats endpoints
323
  async getLicenseStats() {
324
  if (this.isDevMode) {
frontend/js/pages/users.js CHANGED
@@ -584,7 +584,14 @@ class UsersPage {
584
  const status = Utils.getLicenseStatus(license);
585
 
586
  const modal = Components.createModal({
587
- title: '授權詳情',
 
 
 
 
 
 
 
588
  size: 'lg',
589
  content: `
590
  <div class="license-details-modal">
@@ -654,7 +661,7 @@ class UsersPage {
654
  <i class="fas fa-calendar-plus"></i>
655
  延長授權
656
  </button>
657
- <button type="button" class="btn ${license.is_active ? 'btn-warning' : 'btn-success'}"
658
  onclick="usersPage.toggleLicenseStatus('${license.id}', ${license.is_active}); Components.closeModal(this.closest('.modal-overlay'));">
659
  <i class="fas ${license.is_active ? 'fa-pause' : 'fa-play'}"></i>
660
  ${license.is_active ? '停用' : '啟用'}
@@ -740,24 +747,24 @@ class UsersPage {
740
  const formData = new FormData(form);
741
  const extendDays = parseInt(formData.get('extendDays'));
742
  const reason = formData.get('extendReason')?.trim();
743
-
744
  if (!extendDays) {
745
  Utils.showError('請選擇延長天數');
746
  return;
747
  }
748
-
749
  const submitBtn = form.querySelector('button[type="submit"]');
750
  const originalContent = submitBtn.innerHTML;
751
-
752
  try {
753
  submitBtn.disabled = true;
754
  submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 延長中...';
755
-
756
  const response = await api.extendLicense(licenseId, {
757
  extend_days: extendDays,
758
  reason: reason
759
  });
760
-
761
  if (response && response.success) {
762
  Utils.showSuccess('授權延長成功');
763
  Components.closeModal(modal);
@@ -765,7 +772,7 @@ class UsersPage {
765
  } else {
766
  throw new Error(response?.message || '延長授權失敗');
767
  }
768
-
769
  } catch (error) {
770
  console.error('❌ Extend license error:', error);
771
  Utils.handleError(error, '延長授權時');
@@ -773,7 +780,116 @@ class UsersPage {
773
  submitBtn.innerHTML = originalContent;
774
  }
775
  }
776
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
777
  destroy() {
778
  // Cleanup if needed
779
  }
 
584
  const status = Utils.getLicenseStatus(license);
585
 
586
  const modal = Components.createModal({
587
+ title: `
588
+ <div style="display: flex; justify-content: space-between; align-items: center; width: 100%; gap: 12px;">
589
+ <span>授權詳情</span>
590
+ <button class="btn btn-sm btn-secondary" onclick="event.stopPropagation(); Components.closeModal(this.closest('.modal-overlay')); usersPage.showEditLicenseModal('${license.id}')" style="width: 24px; height: 24px; padding: 0; display: flex; align-items: center; justify-content: center;" title="編輯">
591
+ <i class="fas fa-edit" style="font-size: 12px;"></i>
592
+ </button>
593
+ </div>
594
+ `,
595
  size: 'lg',
596
  content: `
597
  <div class="license-details-modal">
 
661
  <i class="fas fa-calendar-plus"></i>
662
  延長授權
663
  </button>
664
+ <button type="button" class="btn ${license.is_active ? 'btn-warning' : 'btn-success'}"
665
  onclick="usersPage.toggleLicenseStatus('${license.id}', ${license.is_active}); Components.closeModal(this.closest('.modal-overlay'));">
666
  <i class="fas ${license.is_active ? 'fa-pause' : 'fa-play'}"></i>
667
  ${license.is_active ? '停用' : '啟用'}
 
747
  const formData = new FormData(form);
748
  const extendDays = parseInt(formData.get('extendDays'));
749
  const reason = formData.get('extendReason')?.trim();
750
+
751
  if (!extendDays) {
752
  Utils.showError('請選擇延長天數');
753
  return;
754
  }
755
+
756
  const submitBtn = form.querySelector('button[type="submit"]');
757
  const originalContent = submitBtn.innerHTML;
758
+
759
  try {
760
  submitBtn.disabled = true;
761
  submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 延長中...';
762
+
763
  const response = await api.extendLicense(licenseId, {
764
  extend_days: extendDays,
765
  reason: reason
766
  });
767
+
768
  if (response && response.success) {
769
  Utils.showSuccess('授權延長成功');
770
  Components.closeModal(modal);
 
772
  } else {
773
  throw new Error(response?.message || '延長授權失敗');
774
  }
775
+
776
  } catch (error) {
777
  console.error('❌ Extend license error:', error);
778
  Utils.handleError(error, '延長授權時');
 
780
  submitBtn.innerHTML = originalContent;
781
  }
782
  }
783
+
784
+ showEditLicenseModal(licenseId) {
785
+ const license = this.licenses.find(l => l.id === licenseId);
786
+ if (!license) return;
787
+
788
+ const modal = Components.createModal({
789
+ title: '編輯授權資訊',
790
+ size: 'md',
791
+ content: `
792
+ <form id="editLicenseForm">
793
+ <div class="form-group">
794
+ <label for="editUserName" class="form-label">
795
+ <i class="fas fa-user"></i>
796
+ 用戶名稱 *
797
+ </label>
798
+ <input type="text"
799
+ id="editUserName"
800
+ name="user_name"
801
+ class="form-input"
802
+ value="${license.user_name || ''}"
803
+ required>
804
+ </div>
805
+
806
+ <div class="form-group">
807
+ <label for="editUserEmail" class="form-label">
808
+ <i class="fas fa-envelope"></i>
809
+ 電子郵件
810
+ </label>
811
+ <input type="email"
812
+ id="editUserEmail"
813
+ name="user_email"
814
+ class="form-input"
815
+ value="${license.user_email || ''}"
816
+ placeholder="user@example.com">
817
+ </div>
818
+
819
+ <div class="form-group">
820
+ <div class="form-info">
821
+ <i class="fas fa-info-circle"></i>
822
+ <span>只能修改用戶名稱和電子郵件,其他資訊不可變更</span>
823
+ </div>
824
+ </div>
825
+
826
+ <div class="modal-actions">
827
+ <button type="button" class="btn btn-secondary" onclick="Components.closeModal(this.closest('.modal-overlay')); usersPage.showLicenseDetails('${licenseId}')">
828
+ 取消
829
+ </button>
830
+ <button type="submit" class="btn btn-primary">
831
+ <i class="fas fa-save"></i>
832
+ 儲存變更
833
+ </button>
834
+ </div>
835
+ </form>
836
+ `
837
+ });
838
+
839
+ // Setup form handler
840
+ const form = modal.querySelector('#editLicenseForm');
841
+ form.addEventListener('submit', async (e) => {
842
+ e.preventDefault();
843
+ await this.handleEditLicense(licenseId, form, modal);
844
+ });
845
+
846
+ Components.showModal(modal);
847
+ }
848
+
849
+ async handleEditLicense(licenseId, form, modal) {
850
+ const formData = new FormData(form);
851
+ const userName = formData.get('user_name')?.trim();
852
+ const userEmail = formData.get('user_email')?.trim();
853
+
854
+ if (!userName) {
855
+ Utils.showError('用戶名稱為必填項');
856
+ return;
857
+ }
858
+
859
+ const submitBtn = form.querySelector('button[type="submit"]');
860
+ const originalContent = submitBtn.innerHTML;
861
+
862
+ try {
863
+ submitBtn.disabled = true;
864
+ submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 更新中...';
865
+
866
+ const updateData = {
867
+ user_name: userName
868
+ };
869
+
870
+ if (userEmail) {
871
+ updateData.user_email = userEmail;
872
+ }
873
+
874
+ const response = await api.updateLicense(licenseId, updateData);
875
+
876
+ if (response && response.success) {
877
+ Utils.showSuccess('授權資訊更新成功');
878
+ Components.closeModal(modal);
879
+ await this.refresh();
880
+ // 更新完成後重新開啟詳情頁面
881
+ this.showLicenseDetails(licenseId);
882
+ } else {
883
+ throw new Error(response?.message || '更新授權失敗');
884
+ }
885
+
886
+ } catch (error) {
887
+ Utils.handleError(error, '更新授權時');
888
+ submitBtn.disabled = false;
889
+ submitBtn.innerHTML = originalContent;
890
+ }
891
+ }
892
+
893
  destroy() {
894
  // Cleanup if needed
895
  }
frontend/versions.html CHANGED
@@ -1335,7 +1335,7 @@
1335
  <i class="fas fa-rocket"></i>
1336
  發布版本
1337
  </button>
1338
- <button type="reset" class="btn btn-secondary">
1339
  <i class="fas fa-undo"></i>
1340
  重置表單
1341
  </button>
@@ -1358,6 +1358,20 @@
1358
 
1359
  if (!form || !dropZone || !fileInput) return;
1360
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1361
  // 檔案上傳事件
1362
  dropZone.addEventListener('click', () => fileInput.click());
1363
 
@@ -1522,6 +1536,7 @@
1522
  Utils.showSuccess('發布成功', `${productType === 'autocad' ? 'AutoCAD' : 'Revit'} 版本 ${version} 已成功發布`);
1523
  e.target.reset();
1524
  this.clearFileSelection();
 
1525
  this.switchPage('versions');
1526
  }
1527
  } catch (error) {
@@ -1533,6 +1548,48 @@
1533
  }
1534
  }
1535
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1536
  async loadDownloadsPage(container) {
1537
  container.innerHTML = `
1538
  <div class="page-section">
 
1335
  <i class="fas fa-rocket"></i>
1336
  發布版本
1337
  </button>
1338
+ <button type="reset" class="btn btn-secondary" onclick="versionApp.clearReleaseFormData()">
1339
  <i class="fas fa-undo"></i>
1340
  重置表單
1341
  </button>
 
1358
 
1359
  if (!form || !dropZone || !fileInput) return;
1360
 
1361
+ // 恢復暫存的表單資料
1362
+ this.restoreReleaseFormData();
1363
+
1364
+ // 監聽表單輸入變化,自動暫存
1365
+ const formInputs = form.querySelectorAll('input, textarea, select');
1366
+ formInputs.forEach(input => {
1367
+ input.addEventListener('input', () => {
1368
+ this.saveReleaseFormData();
1369
+ });
1370
+ input.addEventListener('change', () => {
1371
+ this.saveReleaseFormData();
1372
+ });
1373
+ });
1374
+
1375
  // 檔案上傳事件
1376
  dropZone.addEventListener('click', () => fileInput.click());
1377
 
 
1536
  Utils.showSuccess('發布成功', `${productType === 'autocad' ? 'AutoCAD' : 'Revit'} 版本 ${version} 已成功發布`);
1537
  e.target.reset();
1538
  this.clearFileSelection();
1539
+ this.clearReleaseFormData(); // 清除暫存資料
1540
  this.switchPage('versions');
1541
  }
1542
  } catch (error) {
 
1548
  }
1549
  }
1550
 
1551
+ // 保存發布表單資料到 sessionStorage
1552
+ saveReleaseFormData() {
1553
+ const form = document.getElementById('releaseForm');
1554
+ if (!form) return;
1555
+
1556
+ const formData = {
1557
+ productType: document.getElementById('productType')?.value || '',
1558
+ version: document.getElementById('version')?.value || '',
1559
+ title: document.getElementById('title')?.value || '',
1560
+ changelog: document.getElementById('changelog')?.value || ''
1561
+ };
1562
+
1563
+ sessionStorage.setItem('releaseFormData', JSON.stringify(formData));
1564
+ }
1565
+
1566
+ // 從 sessionStorage 恢復發布表單資料
1567
+ restoreReleaseFormData() {
1568
+ const savedData = sessionStorage.getItem('releaseFormData');
1569
+ if (!savedData) return;
1570
+
1571
+ try {
1572
+ const formData = JSON.parse(savedData);
1573
+
1574
+ const productTypeEl = document.getElementById('productType');
1575
+ const versionEl = document.getElementById('version');
1576
+ const titleEl = document.getElementById('title');
1577
+ const changelogEl = document.getElementById('changelog');
1578
+
1579
+ if (productTypeEl && formData.productType) productTypeEl.value = formData.productType;
1580
+ if (versionEl && formData.version) versionEl.value = formData.version;
1581
+ if (titleEl && formData.title) titleEl.value = formData.title;
1582
+ if (changelogEl && formData.changelog) changelogEl.value = formData.changelog;
1583
+ } catch (error) {
1584
+ // 靜默失敗,不影響使用者體驗
1585
+ }
1586
+ }
1587
+
1588
+ // 清除暫存的發布表單資料
1589
+ clearReleaseFormData() {
1590
+ sessionStorage.removeItem('releaseFormData');
1591
+ }
1592
+
1593
  async loadDownloadsPage(container) {
1594
  container.innerHTML = `
1595
  <div class="page-section">