Files
BigDataTool/templates/db_compare.html
2025-08-12 16:27:00 +08:00

775 lines
39 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cassandra数据比对工具 - DataTools Pro</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
.config-section {
background-color: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.result-section {
margin-top: 30px;
}
.difference-item {
border-left: 4px solid #dc3545;
padding-left: 15px;
margin-bottom: 15px;
background-color: #fff5f5;
padding: 15px;
border-radius: 5px;
}
.stat-card {
text-align: center;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
}
.loading {
display: none;
}
.query-keys {
min-height: 120px;
}
.compare-fields {
min-height: 80px;
}
.exclude-fields {
min-height: 80px;
}
.json-field {
font-family: 'Courier New', monospace;
font-size: 0.9em;
background-color: #f8f9fa !important;
}
.badge {
font-size: 0.7em;
}
.field-container {
border: 1px solid #e9ecef;
border-radius: 5px;
padding: 10px;
margin-bottom: 10px;
}
.field-value {
font-size: 0.85em;
max-height: 200px;
overflow-y: auto;
margin: 0;
}
.field-header {
font-weight: 600;
color: #495057;
}
.pagination {
--bs-pagination-padding-x: 0.5rem;
--bs-pagination-padding-y: 0.25rem;
--bs-pagination-font-size: 0.875rem;
}
.copy-btn {
position: absolute;
top: 5px;
right: 5px;
padding: 2px 6px;
font-size: 0.75rem;
border-radius: 3px;
}
.field-container {
position: relative;
}
/* 确保提示消息在最顶层 */
.alert {
position: fixed !important;
top: 20px !important;
left: 50% !important;
transform: translateX(-50%) !important;
z-index: 9999 !important;
min-width: 300px !important;
max-width: 600px !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
}
/* 树形视图样式 */
.tree-view {
font-family: 'Courier New', monospace;
font-size: 0.9em;
max-height: 500px;
overflow-y: auto;
border: 1px solid #e9ecef;
border-radius: 5px;
padding: 10px;
background-color: #f8f9fa;
}
.tree-node {
margin: 2px 0;
}
.tree-toggle {
cursor: pointer;
user-select: none;
margin-right: 5px;
color: #6c757d;
}
.tree-toggle:hover {
color: #495057;
}
.tree-children {
margin-left: 15px;
}
.tree-item {
margin: 1px 0;
padding: 1px 0;
}
/* 原生数据搜索高亮 */
.raw-data-container mark {
background-color: #fff3cd !important;
padding: 1px 2px;
border-radius: 2px;
}
/* 自定义分页输入框 */
.form-control:focus {
border-color: #86b7fe;
outline: 0;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
/* 树形视图样式 */
.tree-view {
font-family: 'Courier New', monospace;
font-size: 14px;
line-height: 1.5;
padding: 15px;
background-color: #f8f9fa;
border-radius: 5px;
overflow-x: auto;
max-height: 500px;
overflow-y: auto;
}
.tree-key {
color: #0066cc;
font-weight: bold;
}
.tree-value {
color: #008000;
}
.tree-index {
color: #666;
font-style: italic;
}
.tree-object, .tree-array {
margin-left: 20px;
}
/* 原生数据模态框样式 */
#rawDataModal .modal-dialog {
max-width: 90%;
}
#rawDataModal pre {
white-space: pre-wrap;
word-wrap: break-word;
}
#rawDataModal .nav-tabs .nav-link {
color: #495057;
}
#rawDataModal .nav-tabs .nav-link.active {
font-weight: bold;
}
/* 对比视图增强样式 */
#diffView .table-warning td {
background-color: #fff3cd !important;
border-color: #ffc107;
}
#diffView .table-danger td {
background-color: #f8d7da !important;
font-weight: bold;
color: #721c24;
}
#diffView .table-success td {
background-color: #d4edda !important;
color: #155724;
}
#diffView .badge {
font-size: 0.7rem;
}
#diffView .text-truncate {
cursor: pointer;
}
#diffView .text-truncate:hover {
background-color: rgba(0,0,0,0.05);
padding: 2px 4px;
border-radius: 3px;
}
#diffView pre {
font-size: 12px;
line-height: 1.4;
margin: 0;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
}
#diffView .table-warning pre {
background-color: #fff;
border-color: #ffc107;
}
#diffView td {
padding: 0.5rem;
}
/* 分表配置样式 */
.sharding-config-section {
background-color: #e8f4fd;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
border: 2px solid #0d6efd;
}
.sharding-info {
background-color: #f8f9fa;
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
}
.shard-table-info {
display: inline-block;
margin: 5px;
padding: 5px 10px;
background-color: #e7f3ff;
border-radius: 15px;
font-size: 0.9em;
border: 1px solid #b3d9ff;
}
.shard-error-info {
background-color: #ffe6e6;
border-color: #ffb3b3;
color: #d63384;
}
/* 面包屑导航样式 */
.breadcrumb {
background: none;
padding: 0;
}
.breadcrumb-item a {
color: #007bff;
text-decoration: none;
}
</style>
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="/">
<i class="fas fa-database me-2"></i> DataTools Pro
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="/">
<i class="fas fa-home"></i> 首页
</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/cassandra-compare">
<i class="fas fa-database"></i> Cassandra比对
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/redis-compare">
<i class="fab fa-redis"></i> Redis比对
</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container-fluid py-4">
<!-- 面包屑导航 -->
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">首页</a></li>
<li class="breadcrumb-item active" aria-current="page">Cassandra数据比对工具</li>
</ol>
</nav>
<div class="row">
<div class="col-12">
<h1 class="text-center mb-4">
<i class="fas fa-database"></i> Cassandra数据比对工具
<small class="text-muted d-block fs-6 mt-2">支持单表查询和分表查询两种模式</small>
</h1>
</div>
</div>
<div class="row">
<!-- 配置面板 -->
<div class="col-lg-4">
<div class="config-section">
<h4><i class="fas fa-cogs"></i> 配置管理</h4>
<!-- 配置组管理 -->
<div class="card mb-3">
<div class="card-header">
<h6><i class="fas fa-layer-group"></i> 配置组管理</h6>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-8">
<select class="form-select form-select-sm" id="configGroupSelect">
<option value="">选择配置组...</option>
</select>
</div>
<div class="col-4">
<button class="btn btn-primary btn-sm w-100" onclick="loadSelectedConfigGroup()">
<i class="fas fa-download"></i> 加载
</button>
</div>
</div>
<div class="row">
<div class="col-6">
<button class="btn btn-success btn-sm w-100" onclick="showSaveConfigDialog()">
<i class="fas fa-save"></i> 保存配置组
</button>
</div>
<div class="col-6">
<button class="btn btn-info btn-sm w-100" onclick="showManageConfigDialog()">
<i class="fas fa-cog"></i> 管理配置组
</button>
</div>
</div>
<div class="row mt-2">
<div class="col-4">
<button class="btn btn-warning btn-sm w-100" onclick="showQueryHistoryDialog()">
<i class="fas fa-history"></i> 查询历史
</button>
</div>
<div class="col-4">
<button class="btn btn-info btn-sm w-100" onclick="showQueryLogsDialog()">
<i class="fas fa-file-alt"></i> 查询日志
</button>
</div>
<div class="col-4">
<button class="btn btn-secondary btn-sm w-100" onclick="showSaveHistoryDialog()">
<i class="fas fa-bookmark"></i> 保存历史
</button>
</div>
</div>
</div>
</div>
<div class="mb-3">
<button class="btn btn-secondary btn-sm" onclick="loadDefaultConfig()">
<i class="fas fa-refresh"></i> 重置为空配置
</button>
<button class="btn btn-success btn-sm" onclick="exportConfig()">
<i class="fas fa-file-export"></i> 导出配置
</button>
</div>
<!-- 查询模式切换 -->
<div class="card mb-3">
<div class="card-header">
<h6><i class="fas fa-toggle-on"></i> 查询模式</h6>
</div>
<div class="card-body">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="enableSharding" onchange="toggleShardingMode()">
<label class="form-check-label" for="enableSharding">
<strong>启用分表查询模式</strong>
<small class="text-muted d-block">支持TWCS时间分表的智能索引计算</small>
</label>
</div>
</div>
</div>
<!-- 分表参数配置 (默认隐藏) -->
<div class="sharding-config-section" id="shardingConfig" style="display: none;">
<div class="card mb-3">
<div class="card-header">
<h6><i class="fas fa-layer-group"></i> 分表参数配置</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6 class="text-primary">生产环境分表配置</h6>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="use_sharding_for_pro" checked>
<label class="form-check-label" for="use_sharding_for_pro">
启用分表查询
</label>
</div>
<div class="mb-2">
<label for="pro_interval_seconds" class="form-label">时间间隔(秒)</label>
<input type="number" class="form-control form-control-sm" id="pro_interval_seconds" value="604800" min="1">
<small class="form-text text-muted">默认604800秒(7天)</small>
</div>
<div class="mb-2">
<label for="pro_table_count" class="form-label">分表数量</label>
<input type="number" class="form-control form-control-sm" id="pro_table_count" value="14" min="1" max="100">
<small class="form-text text-muted">默认14张分表</small>
</div>
</div>
<div class="col-md-6">
<h6 class="text-success">测试环境分表配置</h6>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="use_sharding_for_test">
<label class="form-check-label" for="use_sharding_for_test">
启用分表查询
</label>
</div>
<div class="mb-2">
<label for="test_interval_seconds" class="form-label">时间间隔(秒)</label>
<input type="number" class="form-control form-control-sm" id="test_interval_seconds" value="604800" min="1">
<small class="form-text text-muted">默认604800秒(7天)</small>
</div>
<div class="mb-2">
<label for="test_table_count" class="form-label">分表数量</label>
<input type="number" class="form-control form-control-sm" id="test_table_count" value="14" min="1" max="100">
<small class="form-text text-muted">默认14张分表</small>
</div>
</div>
</div>
<div class="alert alert-info mt-3">
<strong>分表计算说明:</strong>系统会自动从Key值中提取时间戳并计算分表索引。
计算公式:<code>时间戳 // 间隔秒数 % 分表数量</code>
</div>
</div>
</div>
</div>
<!-- 生产环境配置 -->
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h6><i class="fas fa-server"></i> 生产环境配置</h6>
<button class="btn btn-sm btn-outline-primary" onclick="showImportDialog('pro')">
<i class="fas fa-download"></i> 一键导入
</button>
</div>
<div class="card-body">
<div class="row">
<div class="col-6">
<label class="form-label">集群名称</label>
<input type="text" class="form-control form-control-sm" id="pro_cluster_name" placeholder="Production Cluster">
</div>
<div class="col-6">
<label class="form-label">数据中心</label>
<input type="text" class="form-control form-control-sm" id="pro_datacenter" placeholder="dc1">
</div>
</div>
<div class="row mt-2">
<div class="col-8">
<label class="form-label">集群节点 (逗号分隔)</label>
<input type="text" class="form-control form-control-sm" id="pro_hosts" placeholder="127.0.0.1,127.0.0.2">
</div>
<div class="col-4">
<label class="form-label">端口</label>
<input type="number" class="form-control form-control-sm" id="pro_port" placeholder="9042">
</div>
</div>
<div class="row mt-2">
<div class="col-6">
<label class="form-label">用户名</label>
<input type="text" class="form-control form-control-sm" id="pro_username" placeholder="username">
</div>
<div class="col-6">
<label class="form-label">密码</label>
<input type="password" class="form-control form-control-sm" id="pro_password">
</div>
</div>
<div class="row mt-2">
<div class="col-6">
<label class="form-label">Keyspace</label>
<input type="text" class="form-control form-control-sm" id="pro_keyspace" placeholder="keyspace">
</div>
<div class="col-6">
<label class="form-label">表名</label>
<input type="text" class="form-control form-control-sm" id="pro_table" placeholder="tablename">
<small class="form-text text-muted" id="pro_table_hint">完整表名或基础表名(分表时)</small>
</div>
</div>
</div>
</div>
<!-- 测试环境配置 -->
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h6><i class="fas fa-flask"></i> 测试环境配置</h6>
<button class="btn btn-sm btn-outline-primary" onclick="showImportDialog('test')">
<i class="fas fa-download"></i> 一键导入
</button>
</div>
<div class="card-body">
<div class="row">
<div class="col-6">
<label class="form-label">集群名称</label>
<input type="text" class="form-control form-control-sm" id="test_cluster_name" placeholder="Test Cluster">
</div>
<div class="col-6">
<label class="form-label">数据中心</label>
<input type="text" class="form-control form-control-sm" id="test_datacenter" placeholder="dc1">
</div>
</div>
<div class="row mt-2">
<div class="col-8">
<label class="form-label">集群节点 (逗号分隔)</label>
<input type="text" class="form-control form-control-sm" id="test_hosts" placeholder="127.0.0.1,127.0.0.2">
</div>
<div class="col-4">
<label class="form-label">端口</label>
<input type="number" class="form-control form-control-sm" id="test_port" placeholder="9042">
</div>
</div>
<div class="row mt-2">
<div class="col-6">
<label class="form-label">用户名</label>
<input type="text" class="form-control form-control-sm" id="test_username" placeholder="username">
</div>
<div class="col-6">
<label class="form-label">密码</label>
<input type="password" class="form-control form-control-sm" id="test_password">
</div>
</div>
<div class="row mt-2">
<div class="col-6">
<label class="form-label">Keyspace</label>
<input type="text" class="form-control form-control-sm" id="test_keyspace" placeholder="keyspace">
</div>
<div class="col-6">
<label class="form-label">表名</label>
<input type="text" class="form-control form-control-sm" id="test_table" placeholder="tablename">
<small class="form-text text-muted" id="test_table_hint">完整表名或基础表名(分表时)</small>
</div>
</div>
</div>
</div>
<!-- 查询配置 -->
<div class="card">
<div class="card-header">
<h6><i class="fas fa-search"></i> 查询配置</h6>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">主键字段 (逗号分隔,支持复合主键)</label>
<input type="text" class="form-control form-control-sm" id="keys" placeholder="单主键docid 或 复合主键docid,id" value="">
<small class="form-text text-muted">支持单主键或复合主键复合主键用逗号分隔docid,id</small>
</div>
<div class="mb-3">
<label class="form-label">比较字段 (空则比较全部,逗号分隔)</label>
<textarea class="form-control form-control-sm compare-fields" id="fields_to_compare" placeholder="留空表示比较所有字段&#10;或输入: field1,field2,field3"></textarea>
</div>
<div>
<label class="form-label">排除字段 (逗号分隔)</label>
<textarea class="form-control form-control-sm exclude-fields" id="exclude_fields" placeholder="排除不需要比较的字段&#10;如: field1,field2"></textarea>
</div>
</div>
</div>
</div>
</div>
<!-- 查询面板 -->
<div class="col-lg-8">
<div class="config-section">
<h4><i class="fas fa-key"></i> 查询Key管理</h4>
<div class="mb-3">
<label class="form-label">批量Key输入 (一行一个)</label>
<textarea class="form-control query-keys" id="query_values" placeholder="请输入查询的Key值一行一个&#10;单主键示例:&#10;key1&#10;key2&#10;key3&#10;&#10;复合主键示例(逗号分隔):&#10;docid1,id1&#10;docid2,id2&#10;docid3,id3&#10;&#10;分表查询示例(包含时间戳):&#10;wmid_1609459200&#10;wmid_1610064000&#10;wmid_1610668800"></textarea>
<small class="form-text text-muted" id="key_input_hint">单表模式输入普通Key值 | 分表模式Key值应包含时间戳用于计算分表索引</small>
</div>
<div class="mb-3">
<button class="btn btn-primary" onclick="executeQuery()">
<i class="fas fa-play"></i> <span id="executeButtonText">执行查询比对</span>
</button>
<button class="btn btn-secondary" onclick="clearResults()">
<i class="fas fa-trash"></i> 清空结果
</button>
</div>
<!-- 加载动画 -->
<div class="loading text-center" id="loading">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">查询中...</span>
</div>
<p class="mt-2" id="loadingText">正在执行查询比对...</p>
</div>
</div>
<!-- 结果面板 -->
<div class="result-section" id="results" style="display: none;">
<!-- 分表查询信息 (分表模式时显示) -->
<div class="sharding-info" id="shardingInfoContainer" style="display: none;">
<h6><i class="fas fa-info-circle"></i> 分表查询信息</h6>
<div id="shardingInfo"></div>
</div>
<!-- 统计信息 -->
<div class="row" id="stats">
<!-- 统计卡片将在这里动态生成 -->
</div>
<!-- 结果选项卡导航 -->
<div class="card mt-4">
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs" id="resultTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="differences-tab" data-bs-toggle="tab" data-bs-target="#differences-panel" type="button" role="tab">
<i class="fas fa-exclamation-triangle"></i> 差异详情 <span class="badge bg-danger ms-1" id="diff-count">0</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="identical-tab" data-bs-toggle="tab" data-bs-target="#identical-panel" type="button" role="tab">
<i class="fas fa-check-circle"></i> 相同结果 <span class="badge bg-success ms-1" id="identical-count">0</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="rawdata-tab" data-bs-toggle="tab" data-bs-target="#rawdata-panel" type="button" role="tab">
<i class="fas fa-database"></i> 原始数据 <span class="badge bg-info ms-1" id="rawdata-count">0</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="summary-tab" data-bs-toggle="tab" data-bs-target="#summary-panel" type="button" role="tab">
<i class="fas fa-chart-pie"></i> 比较总结
</button>
</li>
</ul>
<div class="mt-2">
<button class="btn btn-sm btn-outline-primary" onclick="exportResults()">
<i class="fas fa-download"></i> 导出结果
</button>
</div>
</div>
<div class="card-body">
<div class="tab-content" id="resultTabContent">
<!-- 差异详情面板 -->
<div class="tab-pane fade show active" id="differences-panel" role="tabpanel">
<div id="differences">
<!-- 差异内容将在这里动态生成 -->
</div>
</div>
<!-- 相同结果面板 -->
<div class="tab-pane fade" id="identical-panel" role="tabpanel">
<div id="identical-results">
<!-- 相同结果将在这里动态生成 -->
</div>
</div>
<!-- 原始数据面板 -->
<div class="tab-pane fade" id="rawdata-panel" role="tabpanel">
<div class="container-fluid">
<!-- 筛选控制区 -->
<div class="row mb-3">
<div class="col-md-6">
<div class="d-flex align-items-center">
<label class="form-label me-2 mb-0">显示环境:</label>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showProData" checked onchange="filterRawData()">
<label class="form-check-label" for="showProData">生产环境</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showTestData" checked onchange="filterRawData()">
<label class="form-check-label" for="showTestData">测试环境</label>
</div>
</div>
</div>
<div class="col-md-6">
<div class="input-group">
<span class="input-group-text"><i class="fas fa-search"></i></span>
<input type="text" class="form-control" placeholder="搜索Key或字段值..."
onkeyup="searchRawData(this.value)" id="rawDataSearch">
</div>
</div>
</div>
<!-- 原始数据内容 -->
<div id="raw-data-content">
<!-- 原始数据将在这里动态生成 -->
</div>
</div>
</div>
<!-- 比较总结面板 -->
<div class="tab-pane fade" id="summary-panel" role="tabpanel">
<div id="comparison-summary">
<!-- 总结报告将在这里动态生成 -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 查询日志模态框 -->
<div class="modal fade" id="queryLogsModal" tabindex="-1" aria-labelledby="queryLogsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="queryLogsModalLabel">
<i class="fas fa-file-alt"></i> 查询日志管理
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center">
<h6 class="mb-0 me-3">查询执行日志</h6>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="modal-log-level-info" checked onchange="filterModalLogsByLevel()">
<label class="form-check-label text-primary" for="modal-log-level-info">INFO</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="modal-log-level-warning" checked onchange="filterModalLogsByLevel()">
<label class="form-check-label text-warning" for="modal-log-level-warning">WARNING</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="modal-log-level-error" checked onchange="filterModalLogsByLevel()">
<label class="form-check-label text-danger" for="modal-log-level-error">ERROR</label>
</div>
</div>
<div>
<button class="btn btn-sm btn-outline-primary me-2" onclick="refreshModalQueryLogs()">
<i class="fas fa-sync-alt"></i> 刷新
</button>
<button class="btn btn-sm btn-outline-warning me-2" onclick="cleanupOldLogs()">
<i class="fas fa-broom"></i> 清理旧日志
</button>
<button class="btn btn-sm btn-outline-danger" onclick="clearQueryLogs()">
<i class="fas fa-trash"></i> 清空
</button>
</div>
</div>
<div id="modal-query-logs" style="max-height: 600px; overflow-y: auto;">
<!-- 查询日志将在这里动态生成 -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
</body>
</html>