refactor(templates): 統一模板目錄並移除 fallback loader

ADR-017 Phase 3f-4:根目錄模板搬入 templates/,補 trends/login_history,移除 ChoiceLoader 根目錄 fallback,搬移 components,刪除 web/templates 下的空檔/死檔與 compose 舊模板 mount。
This commit is contained in:
OoO
2026-04-29 21:44:19 +08:00
parent 9528d6c23e
commit 53edcc0077
30 changed files with 227 additions and 1662 deletions

12
app.py
View File

@@ -131,25 +131,17 @@ if NGROK_AUTH_TOKEN == '36e27NM5V7sUJ8QxJIAAWCp7sUv_3brtcrBarYvcP3SbvFKhF':
sys_log.warning("[Security] ⚠️ 使用預設 NGROK_AUTH_TOKEN請設定環境變數")
conf.get_default().auth_token = NGROK_AUTH_TOKEN
TEMPLATE_DIR = BASE_DIR # 修正:根據檔案結構,模板位於根目錄
TEMPLATE_DIR_NEW = os.path.join(BASE_DIR, 'templates') # 新模板路徑(模組化)
TEMPLATE_DIR = os.path.join(BASE_DIR, 'templates')
STATIC_DIR = os.path.join(BASE_DIR, 'web/static')
# 檢查關鍵模板是否存在
if not os.path.exists(os.path.join(BASE_DIR, 'dashboard.html')):
if not os.path.exists(os.path.join(TEMPLATE_DIR, 'dashboard.html')):
sys_log.warning(f"[Web] [Template] ⚠️ 警告: 找不到 dashboard.html | Path: {TEMPLATE_DIR}")
app = Flask(__name__,
template_folder=TEMPLATE_DIR,
static_folder=STATIC_DIR)
# 設定多路徑模板載入器(同時支援根目錄和 templates/ 目錄)
from jinja2 import FileSystemLoader, ChoiceLoader
app.jinja_loader = ChoiceLoader([
FileSystemLoader(TEMPLATE_DIR_NEW), # templates/ 目錄優先
FileSystemLoader(TEMPLATE_DIR), # 根目錄備用
])
# ==========================================
# 🔒 Flask 安全配置
# ==========================================

View File

@@ -67,27 +67,6 @@ services:
# HTML 模板 (熱更新)
- ./templates:/app/templates:ro
- ./web/templates:/app/web/templates:ro
- ./login.html:/app/login.html:ro
- ./dashboard.html:/app/dashboard.html:ro
- ./daily_sales.html:/app/daily_sales.html:ro
- ./sales_analysis.html:/app/sales_analysis.html:ro
- ./growth_analysis.html:/app/growth_analysis.html:ro
- ./monthly_summary_analysis.html:/app/monthly_summary_analysis.html:ro
- ./edm_dashboard.html:/app/edm_dashboard.html:ro
- ./index.html:/app/index.html:ro
- ./logs.html:/app/logs.html:ro
- ./auto_import_index.html:/app/auto_import_index.html:ro
- ./settings.html:/app/settings.html:ro
- ./system_settings.html:/app/system_settings.html:ro
# 其他根目錄路由
- ./auto_import_routes.py:/app/auto_import_routes.py:ro
- ./crawler_management_routes.py:/app/crawler_management_routes.py:ro
- ./import.html:/app/import.html:ro
# AI 助手模板及相關依賴
- ./templates/ai_recommend.html:/app/ai_recommend.html:ro
- ./templates/ai_history.html:/app/ai_history.html:ro
- ./templates/base.html:/app/base.html:ro
- ./web/templates/components:/app/components:ro
environment:
- FLASK_ENV=production
- PYTHONUNBUFFERED=1

1183
logs.html

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
../web/templates/components

View File

@@ -0,0 +1,90 @@
{% extends "base.html" %}
{% block title %}登入歷史 - WOOO TECH{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="page-header">
<h1><i class="fas fa-clock-rotate-left me-2"></i>登入歷史</h1>
<p>系統登入記錄與異常嘗試追蹤</p>
</div>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="fas fa-list me-2"></i>最近記錄</span>
<select class="form-select form-select-sm" id="historyLimit" style="width: 120px;">
<option value="50">50 筆</option>
<option value="100" selected>100 筆</option>
<option value="200">200 筆</option>
</select>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>時間</th>
<th>帳號</th>
<th>狀態</th>
<th>IP</th>
<th>原因</th>
<th>User Agent</th>
</tr>
</thead>
<tbody id="loginHistoryBody">
<tr>
<td colspan="6" class="text-center py-4">
<div class="spinner-border text-primary" role="status"></div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
const statusClass = {
success: 'text-bg-success',
failed: 'text-bg-danger',
locked: 'text-bg-warning'
};
function escapeHtml(value) {
return String(value ?? '').replace(/[&<>"']/g, char => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
}[char]));
}
async function loadLoginHistory() {
const limit = document.getElementById('historyLimit').value;
const tbody = document.getElementById('loginHistoryBody');
const response = await fetch(`/api/login_history?limit=${limit}`);
const result = await response.json();
if (!result.success) {
tbody.innerHTML = `<tr><td colspan="6" class="text-center text-danger py-4">${escapeHtml(result.message)}</td></tr>`;
return;
}
if (!result.data.length) {
tbody.innerHTML = '<tr><td colspan="6" class="text-center text-muted py-4">尚無登入記錄</td></tr>';
return;
}
tbody.innerHTML = result.data.map(item => `
<tr>
<td>${escapeHtml(item.login_time ? new Date(item.login_time).toLocaleString('zh-TW') : '')}</td>
<td>${escapeHtml(item.username_attempted || item.user_id || '')}</td>
<td><span class="badge ${statusClass[item.status] || 'text-bg-secondary'}">${escapeHtml(item.status)}</span></td>
<td>${escapeHtml(item.ip_address)}</td>
<td>${escapeHtml(item.failure_reason)}</td>
<td class="text-truncate" style="max-width: 360px;">${escapeHtml(item.user_agent)}</td>
</tr>
`).join('');
}
document.getElementById('historyLimit').addEventListener('change', loadLoginHistory);
document.addEventListener('DOMContentLoaded', loadLoginHistory);
</script>
{% endblock %}

135
templates/trends.html Normal file
View File

@@ -0,0 +1,135 @@
{% extends "base.html" %}
{% block title %}趨勢資料 - WOOO TECH{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="page-header">
<h1><i class="fas fa-chart-line me-2"></i>趨勢資料</h1>
<p>Google News、PTT、Dcard、YouTube 趨勢訊號</p>
</div>
<div class="row g-3 mb-3">
<div class="col-md-3">
<select class="form-select" id="sourceFilter">
<option value="">全部來源</option>
<option value="google_news">Google News</option>
<option value="ptt">PTT</option>
<option value="dcard">Dcard</option>
<option value="youtube">YouTube</option>
</select>
</div>
<div class="col-md-3">
<input class="form-control" id="categoryFilter" placeholder="分類">
</div>
<div class="col-md-3">
<select class="form-select" id="daysFilter">
<option value="1">近 1 天</option>
<option value="7" selected>近 7 天</option>
<option value="30">近 30 天</option>
</select>
</div>
<div class="col-md-3">
<button class="btn btn-primary w-100" id="refreshBtn">
<i class="fas fa-rotate me-1"></i>更新
</button>
</div>
</div>
<div class="row g-3">
<div class="col-lg-4">
<div class="card h-100">
<div class="card-header"><i class="fas fa-fire me-2"></i>熱門關鍵字</div>
<div class="card-body" id="keywordsBox">
<div class="text-center py-4"><div class="spinner-border text-primary" role="status"></div></div>
</div>
</div>
</div>
<div class="col-lg-8">
<div class="card h-100">
<div class="card-header"><i class="fas fa-newspaper me-2"></i>趨勢記錄</div>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>日期</th>
<th>來源</th>
<th>分類</th>
<th>標題</th>
<th>熱度</th>
</tr>
</thead>
<tbody id="recordsBody">
<tr><td colspan="5" class="text-center py-4"><div class="spinner-border text-primary" role="status"></div></td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
function escapeHtml(value) {
return String(value ?? '').replace(/[&<>"']/g, char => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
}[char]));
}
function filters() {
const params = new URLSearchParams();
const source = document.getElementById('sourceFilter').value;
const category = document.getElementById('categoryFilter').value.trim();
const days = document.getElementById('daysFilter').value;
if (source) params.set('source', source);
if (category) params.set('category', category);
params.set('days', days);
return params;
}
async function loadTrends() {
const params = filters();
params.set('limit', '50');
const [recordsResponse, keywordsResponse] = await Promise.all([
fetch(`/api/trends/records?${params}`),
fetch(`/api/trends/keywords?${params}`)
]);
const records = await recordsResponse.json();
const keywords = await keywordsResponse.json();
const recordsBody = document.getElementById('recordsBody');
if (!records.success || !records.data.length) {
recordsBody.innerHTML = '<tr><td colspan="5" class="text-center text-muted py-4">尚無趨勢資料</td></tr>';
} else {
recordsBody.innerHTML = records.data.map(item => `
<tr>
<td>${escapeHtml(item.trend_date)}</td>
<td>${escapeHtml(item.source)}</td>
<td>${escapeHtml(item.category)}</td>
<td class="text-truncate" style="max-width: 420px;">${escapeHtml(item.title || item.keyword)}</td>
<td>${escapeHtml(item.popularity_score || '')}</td>
</tr>
`).join('');
}
const keywordsBox = document.getElementById('keywordsBox');
if (!keywords.success || !keywords.data.length) {
keywordsBox.innerHTML = '<div class="text-muted text-center py-4">尚無關鍵字</div>';
} else {
keywordsBox.innerHTML = keywords.data.map(item => `
<div class="d-flex justify-content-between align-items-center border-bottom py-2">
<span>${escapeHtml(item.keyword)}</span>
<span class="badge text-bg-primary">${escapeHtml(item.total_mentions || 0)}</span>
</div>
`).join('');
}
}
document.getElementById('refreshBtn').addEventListener('click', loadTrends);
document.getElementById('sourceFilter').addEventListener('change', loadTrends);
document.getElementById('daysFilter').addEventListener('change', loadTrends);
document.addEventListener('DOMContentLoaded', loadTrends);
</script>
{% endblock %}

View File

@@ -1,197 +0,0 @@
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WOOO TECH 品牌資產庫</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
background: #f8f9fa;
color: #333;
margin: 0;
padding: 40px;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
h1 {
text-align: center;
margin-bottom: 40px;
color: #1a1a1a;
}
.section {
background: white;
border-radius: 12px;
padding: 30px;
margin-bottom: 30px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
}
.section-title {
font-size: 1.5rem;
margin-bottom: 20px;
border-bottom: 2px solid #eee;
padding-bottom: 10px;
}
.asset-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 30px;
align-items: end;
}
.asset-item {
text-align: center;
}
.preview-box {
background: #fff;
border: 1px solid #eee;
padding: 20px;
border-radius: 8px;
margin-bottom: 15px;
height: 150px;
display: flex;
align-items: center;
justify-content: center;
/* 棋盤格背景以顯示透明度 */
background-image: linear-gradient(45deg, #eee 25%, transparent 25%), linear-gradient(-45deg, #eee 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #eee 75%), linear-gradient(-45deg, transparent 75%, #eee 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
}
.preview-box img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.format-links a {
display: inline-block;
margin: 5px;
text-decoration: none;
color: #4F46E5;
font-size: 0.9rem;
border: 1px solid #4F46E5;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.2s;
}
.format-links a:hover {
background: #4F46E5;
color: white;
}
</style>
</head>
<body>
<div class="container">
<h1>WOOO TECH 品牌資產庫</h1>
<div class="section">
<h2 class="section-title">1. 主品牌標誌 (Main Logo)</h2>
<div class="asset-grid">
<div class="asset-item">
<div class="preview-box">
<img src="/static/exports/WOOO_Main_Logo.svg" alt="Main Logo">
</div>
<div>
<strong>SVG (向量)</strong><br>
<div class="format-links">
<a href="/static/exports/WOOO_Main_Logo.svg" download>下載 SVG</a>
</div>
</div>
</div>
<div class="asset-item">
<div class="preview-box">
<img src="/static/exports/WOOO_Main_Logo.jpg" alt="Main Logo JPG">
</div>
<div>
<strong>JPG (白底)</strong><br>
<div class="format-links">
<a href="/static/exports/WOOO_Main_Logo.jpg" download>下載 JPG</a>
</div>
</div>
</div>
<!-- 其他格式僅提供下載 -->
<div class="asset-item">
<div style="padding: 20px;">
<strong>其他格式</strong><br>
<div class="format-links">
<a href="/static/exports/WOOO_Main_Logo.eps" download>EPS</a>
<a href="/static/exports/WOOO_Main_Logo.pdf" download>PDF/AI</a>
<a href="/static/exports/WOOO_Main_Logo.tiff" download>TIFF</a>
</div>
</div>
</div>
</div>
</div>
<div class="section">
<h2 class="section-title">2. 玻璃質感版 (Glass Version)</h2>
<div class="asset-grid">
<div class="asset-item">
<div class="preview-box">
<img src="/static/exports/WOOO_Glass_Logo.svg" alt="Glass Logo">
</div>
<div>
<strong>SVG (向量)</strong><br>
<div class="format-links">
<a href="/static/exports/WOOO_Glass_Logo.svg" download>下載 SVG</a>
</div>
</div>
</div>
<div class="asset-item">
<div class="preview-box">
<img src="/static/exports/WOOO_Glass_Logo.jpg" alt="Glass Logo JPG">
</div>
<div>
<strong>JPG (白底)</strong><br>
<div class="format-links">
<a href="/static/exports/WOOO_Glass_Logo.jpg" download>下載 JPG</a>
</div>
</div>
</div>
</div>
</div>
<div class="section">
<h2 class="section-title">3. 能量流動版 (Gradient Version)</h2>
<div class="asset-grid">
<div class="asset-item">
<div class="preview-box">
<img src="/static/exports/WOOO_Gradient_Logo.svg" alt="Gradient Logo">
</div>
<div>
<strong>SVG (向量)</strong><br>
<div class="format-links">
<a href="/static/exports/WOOO_Gradient_Logo.svg" download>下載 SVG</a>
</div>
</div>
</div>
<div class="asset-item">
<div class="preview-box">
<img src="/static/exports/WOOO_Gradient_Logo.jpg" alt="Gradient Logo JPG">
</div>
<div>
<strong>JPG (白底)</strong><br>
<div class="format-links">
<a href="/static/exports/WOOO_Gradient_Logo.jpg" download>下載 JPG</a>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,250 +0,0 @@
<!-- cspell:ignore MOMO -->
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>營運成長報表 - MOMO 監控系統</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
<style>
body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background-color: #f4f6f9; }
.navbar { box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
.card { border: none; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.03); margin-bottom: 1.5rem; transition: all 0.3s ease; background: #fff; }
.card:hover { transform: translateY(-2px); box-shadow: 0 8px 25px rgba(0,0,0,0.08); }
.card-header { background-color: transparent; border-bottom: 1px solid rgba(0,0,0,0.05); font-weight: 700; color: #2c3e50; padding: 1.25rem; }
.kpi-card { position: relative; overflow: hidden; border: none; }
.kpi-card .icon-bg { position: absolute; right: -15px; bottom: -15px; font-size: 6rem; opacity: 0.15; transform: rotate(-15deg); pointer-events: none; }
.kpi-value { font-size: 2rem; font-weight: 800; letter-spacing: -0.5px; margin-bottom: 0.2rem; }
.kpi-label { font-size: 0.85rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; opacity: 0.9; }
.trend-up { color: #2ecc71; }
.trend-down { color: #e74c3c; }
</style>
</head>
<body class="bg-body-tertiary">
{% include 'components/_navbar.html' %}
<div class="container-fluid px-4">
<div class="d-flex justify-content-between align-items-center mb-4 mt-4">
<h4 class="mb-0 fw-bold text-dark"><i class="fas fa-rocket me-2 text-success"></i>營運成長策略報表</h4>
<span class="text-muted small">數據更新至: {{ chart_data.labels[-1] if chart_data.labels else '-' }}</span>
</div>
<!-- KPI Cards -->
<div class="row mb-4">
<div class="col-md-4">
<div class="card kpi-card bg-primary text-white h-100 shadow-sm">
<div class="card-body p-4">
<div class="kpi-label text-white-50">YTD 本年度累計業績 ({{ kpi.current_year }})</div>
<div class="kpi-value">${{ "{:,.0f}".format(kpi.ytd_revenue) }}</div>
<div class="mt-2">
<span class="badge bg-white text-primary me-2">YoY Growth</span>
<span class="fw-bold {{ 'text-white' if kpi.ytd_growth >= 0 else 'text-warning' }}">
<i class="fas fa-{{ 'arrow-up' if kpi.ytd_growth >= 0 else 'arrow-down' }} me-1"></i>
{{ "{:+.1f}%".format(kpi.ytd_growth) }}
</span>
<span class="small text-white-50 ms-1">vs 去年同期</span>
</div>
<i class="fas fa-chart-line icon-bg"></i>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card kpi-card bg-success text-white h-100 shadow-sm">
<div class="card-body p-4">
<div class="kpi-label text-white-50">近30天平均客單價 (AOV)</div>
<div class="kpi-value">${{ "{:,.0f}".format(kpi.recent_aov) }}</div>
<div class="mt-2 small text-white-50">
真實訂單基礎 (Unique Order ID)
</div>
<i class="fas fa-shopping-cart icon-bg"></i>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card kpi-card bg-info text-white h-100 shadow-sm">
<div class="card-body p-4">
<div class="kpi-label text-white-50">總訂單數 (Total Orders)</div>
<div class="kpi-value">{{ "{:,.0f}".format(kpi.total_orders) }}</div>
<div class="mt-2 small text-white-50">
全時期累計
</div>
<i class="fas fa-receipt icon-bg"></i>
</div>
</div>
</div>
</div>
<!-- Charts Row 1: Revenue & Growth -->
<div class="row mb-4">
<div class="col-lg-8">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="fas fa-chart-bar me-2"></i>月營收與年增率 (Revenue & YoY)</span>
</div>
<div class="card-body">
<div style="height: 350px;">
<canvas id="revenueChart"></canvas>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card h-100">
<div class="card-header">
<i class="fas fa-percentage me-2"></i>月增率分析 (MoM)
</div>
<div class="card-body">
<div style="height: 350px;">
<canvas id="momChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<!-- Charts Row 2: AOV & Profit -->
<div class="row mb-4">
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header">
<i class="fas fa-wallet me-2"></i>客單價趨勢 (AOV Trend)
</div>
<div class="card-body">
<div style="height: 300px;">
<canvas id="aovChart"></canvas>
</div>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header">
<i class="fas fa-hand-holding-usd me-2"></i>獲利能力分析 (Gross Margin %)
</div>
<div class="card-body">
<div style="height: 300px;">
<canvas id="marginChart"></canvas>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- Data Injection -->
<script id="chart-data" type="application/json">
{{ chart_data | tojson }}
</script>
<script>
const data = JSON.parse(document.getElementById('chart-data').textContent);
// 1. Revenue & YoY Chart (Mixed)
new Chart(document.getElementById('revenueChart'), {
type: 'bar',
data: {
labels: data.labels,
datasets: [
{
label: '月營收 ($)',
data: data.revenue,
backgroundColor: 'rgba(54, 162, 235, 0.6)',
order: 2
},
{
label: 'YoY 年增率 (%)',
data: data.yoy,
type: 'line',
borderColor: '#ff6384',
borderWidth: 2,
yAxisID: 'y1',
order: 1,
tension: 0.3
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: { beginAtZero: true, title: {display: true, text: '金額 ($)'} },
y1: {
position: 'right',
grid: { drawOnChartArea: false },
title: {display: true, text: '成長率 (%)'}
}
}
}
});
// 2. MoM Chart
new Chart(document.getElementById('momChart'), {
type: 'bar',
data: {
labels: data.labels,
datasets: [{
label: 'MoM 月增率 (%)',
data: data.mom,
backgroundColor: (ctx) => {
const val = ctx.raw;
return val >= 0 ? 'rgba(75, 192, 192, 0.6)' : 'rgba(255, 99, 132, 0.6)';
}
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } }
}
});
// 3. AOV Chart
new Chart(document.getElementById('aovChart'), {
type: 'line',
data: {
labels: data.labels,
datasets: [{
label: '平均客單價 ($)',
data: data.aov,
borderColor: '#36a2eb',
backgroundColor: 'rgba(54, 162, 235, 0.1)',
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: { y: { beginAtZero: true } }
}
});
// 4. Margin Rate Chart
new Chart(document.getElementById('marginChart'), {
type: 'line',
data: {
labels: data.labels,
datasets: [{
label: '毛利率 (%)',
data: data.margin_rate,
borderColor: '#2ecc71',
backgroundColor: 'rgba(46, 204, 113, 0.1)',
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: { y: { beginAtZero: true } }
}
});
</script>
</body>
</html>