web server
This commit is contained in:
120
web-server/views/application-detail.ejs
Normal file
120
web-server/views/application-detail.ejs
Normal file
@@ -0,0 +1,120 @@
|
||||
<%- include('partials/page-start') %>
|
||||
|
||||
<section class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="breadcrumb"><a href="/applications">Applications</a><span>/</span><span><%= application.code %></span></div>
|
||||
<h1><%= application.name %></h1>
|
||||
<p><%= application.notes %></p>
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
type="button"
|
||||
data-app-edit
|
||||
data-app-id="<%= application.id %>"
|
||||
data-app-code="<%= application.code %>"
|
||||
data-app-name="<%= application.name %>"
|
||||
data-app-version="<%= application.version %>"
|
||||
data-app-status="<%= application.status %>"
|
||||
data-app-notes="<%= application.notes %>"
|
||||
data-app-packages="<%= JSON.stringify(application.packages.map((pkg) => ({ packageId: pkg.packageId, selectedVersionId: pkg.selectedVersionId }))) %>"
|
||||
>
|
||||
<span class="material-symbols-outlined">edit</span>
|
||||
Sửa App
|
||||
</button>
|
||||
<form method="post" action="/applications/<%= application.id %>/release" data-confirm-submit="Chuyển app <%= application.code %> sang Released?">
|
||||
<input type="hidden" name="returnTo" value="<%= currentPath %>">
|
||||
<button class="btn btn-primary" type="submit" <%= application.status === 'Released' ? 'disabled' : '' %>>
|
||||
<span class="material-symbols-outlined">archive</span>
|
||||
Đóng gói
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" action="/applications/<%= application.id %>/delete" data-confirm-submit="Xóa app <%= application.code %> khỏi hệ thống?">
|
||||
<button class="btn btn-danger" type="submit">
|
||||
<span class="material-symbols-outlined">delete</span>
|
||||
Xóa
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-grid">
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h2>Thông tin App</h2>
|
||||
<p>Thông tin dùng ở danh sách và pipeline đóng gói.</p>
|
||||
</div>
|
||||
</div>
|
||||
<dl class="detail-list">
|
||||
<div><dt>Code</dt><dd><%= application.code %></dd></div>
|
||||
<div><dt>Version</dt><dd><strong><%= application.version %></strong></dd></div>
|
||||
<div><dt>Package count</dt><dd><%= application.packageCount %></dd></div>
|
||||
<div><dt>Created date</dt><dd><%= application.createdAt %></dd></div>
|
||||
<div><dt>Created by</dt><dd><%= application.createdBy %></dd></div>
|
||||
<div><dt>Status</dt><dd><span class="badge <%= helpers.statusClass(application.status) %>"><%= application.status %></span></dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="panel wide-panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h2>Package trong App</h2>
|
||||
<p>Mỗi package có thể chọn version cụ thể cho app này.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap compact">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Package</th>
|
||||
<th>Type</th>
|
||||
<th>Selected version</th>
|
||||
<th class="action-col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% application.packages.forEach((pkg) => { %>
|
||||
<tr>
|
||||
<td>
|
||||
<strong><%= pkg.name %></strong>
|
||||
<span class="table-subtitle"><%= pkg.code %></span>
|
||||
</td>
|
||||
<td><span class="badge <%= helpers.packageTypeClass(pkg.type) %>"><%= helpers.packageTypeLabel(pkg.type) %></span></td>
|
||||
<td><%= pkg.selectedVersion %></td>
|
||||
<td class="action-col">
|
||||
<div class="action-group">
|
||||
<button
|
||||
class="icon-button subtle"
|
||||
type="button"
|
||||
title="Đổi version"
|
||||
data-app-edit
|
||||
data-app-id="<%= application.id %>"
|
||||
data-app-code="<%= application.code %>"
|
||||
data-app-name="<%= application.name %>"
|
||||
data-app-version="<%= application.version %>"
|
||||
data-app-status="<%= application.status %>"
|
||||
data-app-notes="<%= application.notes %>"
|
||||
data-app-packages="<%= JSON.stringify(application.packages.map((item) => ({ packageId: item.packageId, selectedVersionId: item.selectedVersionId }))) %>"
|
||||
>
|
||||
<span class="material-symbols-outlined">swap_horiz</span>
|
||||
</button>
|
||||
<form method="post" action="/applications/<%= application.id %>/packages/<%= pkg.packageId %>/delete" data-confirm-submit="Gỡ package <%= pkg.code %> khỏi app?">
|
||||
<button class="icon-button danger" type="submit" title="Gỡ package" aria-label="Gỡ package <%= pkg.name %>">
|
||||
<span class="material-symbols-outlined">link_off</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<%- include('partials/edit-app-modal') %>
|
||||
<%- include('partials/page-end') %>
|
||||
114
web-server/views/applications.ejs
Normal file
114
web-server/views/applications.ejs
Normal file
@@ -0,0 +1,114 @@
|
||||
<%- include('partials/page-start') %>
|
||||
|
||||
<section class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>Applications</h1>
|
||||
<p>Danh sách app được tạo từ các package đã chọn, kèm version và ghi chú đóng gói.</p>
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
<a class="btn btn-secondary" href="/applications/export.csv">
|
||||
<span class="material-symbols-outlined">download</span>
|
||||
Export
|
||||
</a>
|
||||
<a class="btn btn-primary" href="/builder">
|
||||
<span class="material-symbols-outlined">add</span>
|
||||
Tạo App
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-filters">
|
||||
<label class="filter-field">
|
||||
<span>Status</span>
|
||||
<select data-filter-select data-filter-column="status" data-filter-table="applicationsTable">
|
||||
<option value="">Tất cả</option>
|
||||
<option value="Draft">Draft</option>
|
||||
<option value="Released">Released</option>
|
||||
<option value="Archived">Archived</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="filter-field wide">
|
||||
<span>Search</span>
|
||||
<input type="search" placeholder="Tìm theo tên app, code, người tạo..." data-table-search="applicationsTable">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<section class="table-panel">
|
||||
<div class="table-wrap">
|
||||
<table id="applicationsTable" class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Application</th>
|
||||
<th>Version</th>
|
||||
<th>Packages</th>
|
||||
<th>Created date</th>
|
||||
<th>Created by</th>
|
||||
<th>Status</th>
|
||||
<th>Notes</th>
|
||||
<th class="action-col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (applications.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="8" class="table-empty">Chưa có app trong database. Tạo app sau khi đã upload package.</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
<% applications.forEach((item) => { %>
|
||||
<tr data-search="<%= `${item.name} ${item.code} ${item.version} ${item.createdBy} ${item.notes}`.toLowerCase() %>" data-status="<%= item.status %>">
|
||||
<td>
|
||||
<a class="table-title" href="/applications/<%= item.id %>"><%= item.name %></a>
|
||||
<span class="table-subtitle"><%= item.code %></span>
|
||||
</td>
|
||||
<td><strong><%= item.version %></strong></td>
|
||||
<td><%= item.packageCount %></td>
|
||||
<td><%= item.createdAt %></td>
|
||||
<td><%= item.createdBy %></td>
|
||||
<td><span class="badge <%= helpers.statusClass(item.status) %>"><%= item.status %></span></td>
|
||||
<td class="notes-cell"><%= item.notes %></td>
|
||||
<td class="action-col">
|
||||
<div class="action-group">
|
||||
<a class="icon-button subtle" href="/applications/<%= item.id %>" title="Xem chi tiết" aria-label="Xem chi tiết <%= item.name %>">
|
||||
<span class="material-symbols-outlined">visibility</span>
|
||||
</a>
|
||||
<button
|
||||
class="icon-button subtle"
|
||||
type="button"
|
||||
title="Sửa app"
|
||||
data-app-edit
|
||||
data-app-id="<%= item.id %>"
|
||||
data-app-code="<%= item.code %>"
|
||||
data-app-name="<%= item.name %>"
|
||||
data-app-version="<%= item.version %>"
|
||||
data-app-status="<%= item.status %>"
|
||||
data-app-notes="<%= item.notes %>"
|
||||
data-app-packages="<%= JSON.stringify(item.packages.map((pkg) => ({ packageId: pkg.packageId, selectedVersionId: pkg.selectedVersionId }))) %>"
|
||||
>
|
||||
<span class="material-symbols-outlined">edit</span>
|
||||
</button>
|
||||
<form method="post" action="/applications/<%= item.id %>/delete" data-confirm-submit="Xóa app <%= item.code %>? Thao tác này sẽ xóa thông tin đóng gói của app.">
|
||||
<button class="icon-button danger" type="submit" title="Xóa app" aria-label="Xóa app <%= item.name %>">
|
||||
<span class="material-symbols-outlined">delete</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="page-pager">
|
||||
<span>Showing 1-<%= applications.length %> of <%= applications.length %></span>
|
||||
<div>
|
||||
<button type="button" disabled>Prev</button>
|
||||
<span>Page 1 / 1</span>
|
||||
<button type="button" disabled>Next</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<%- include('partials/edit-app-modal') %>
|
||||
<%- include('partials/page-end') %>
|
||||
97
web-server/views/auth.ejs
Normal file
97
web-server/views/auth.ejs
Normal file
@@ -0,0 +1,97 @@
|
||||
<!doctype html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><%= title %> | Robot Installer</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/vendor/notiflix/notiflix-<%= notiflixVersion %>.min.css">
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
</head>
|
||||
<body class="auth-shell" <% if (notice) { %>data-notice-type="<%= notice.type %>" data-notice="<%= notice.message %>"<% } %>>
|
||||
<main class="auth-page">
|
||||
<section class="auth-panel">
|
||||
<div class="auth-brand">
|
||||
<div class="brand-mark">
|
||||
<span class="material-symbols-outlined">precision_manufacturing</span>
|
||||
</div>
|
||||
<div class="brand-copy">
|
||||
<strong>Robot Installer</strong>
|
||||
<span>User Access</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if (mode === 'login') { %>
|
||||
<div class="auth-heading">
|
||||
<h1>Đăng nhập</h1>
|
||||
<p>Truy cập console quản lý package và app.</p>
|
||||
</div>
|
||||
|
||||
<form class="auth-form" method="post" action="/login">
|
||||
<input type="hidden" name="returnTo" value="<%= returnTo %>">
|
||||
<label class="form-field">
|
||||
<span>Username hoặc email</span>
|
||||
<input type="text" name="identifier" autocomplete="username" required autofocus>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Mật khẩu</span>
|
||||
<input type="password" name="password" autocomplete="current-password" required>
|
||||
</label>
|
||||
<button class="btn btn-primary auth-submit" type="submit">
|
||||
<span class="material-symbols-outlined">login</span>
|
||||
Đăng nhập
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="auth-switch">Chưa có tài khoản? <a href="/register">Đăng ký</a></p>
|
||||
<% } else { %>
|
||||
<div class="auth-heading">
|
||||
<h1>Đăng ký</h1>
|
||||
<p>App sẽ gửi email xác nhận để kích hoạt tài khoản.</p>
|
||||
</div>
|
||||
|
||||
<form class="auth-form" method="post" action="/register" data-register-form>
|
||||
<div class="form-grid">
|
||||
<label class="form-field">
|
||||
<span>Username</span>
|
||||
<input type="text" name="username" value="<%= values.username || '' %>" autocomplete="username" required autofocus data-unique-check="username">
|
||||
<small class="field-feedback" data-unique-feedback="username"></small>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Họ tên</span>
|
||||
<input type="text" name="fullName" value="<%= values.fullName || '' %>" autocomplete="name">
|
||||
</label>
|
||||
</div>
|
||||
<label class="form-field">
|
||||
<span>Email</span>
|
||||
<input type="email" name="email" value="<%= values.email || '' %>" autocomplete="email" required data-unique-check="email">
|
||||
<small class="field-feedback" data-unique-feedback="email"></small>
|
||||
</label>
|
||||
<div class="form-grid">
|
||||
<label class="form-field">
|
||||
<span>Mật khẩu</span>
|
||||
<input type="password" name="password" autocomplete="new-password" minlength="8" required>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Xác nhận mật khẩu</span>
|
||||
<input type="password" name="confirmPassword" autocomplete="new-password" minlength="8" required>
|
||||
</label>
|
||||
</div>
|
||||
<button class="btn btn-primary auth-submit" type="submit" data-register-submit>
|
||||
<span class="material-symbols-outlined">person_add</span>
|
||||
Tạo tài khoản
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="auth-switch">Đã có tài khoản? <a href="/login">Đăng nhập</a></p>
|
||||
<% } %>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/vendor/notiflix/notiflix-<%= notiflixVersion %>.min.js"></script>
|
||||
<script src="/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
105
web-server/views/builder.ejs
Normal file
105
web-server/views/builder.ejs
Normal file
@@ -0,0 +1,105 @@
|
||||
<%- include('partials/page-start') %>
|
||||
|
||||
<section class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>Đóng gói App</h1>
|
||||
<p>Tạo app bằng cách chọn package `.deb` hoặc Docker và gán version cụ thể.</p>
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
<button class="btn btn-secondary" type="submit" form="builderForm" name="status" value="Draft">
|
||||
<span class="material-symbols-outlined">draft</span>
|
||||
Lưu nháp
|
||||
</button>
|
||||
<button class="btn btn-primary" type="submit" form="builderForm">
|
||||
<span class="material-symbols-outlined">save</span>
|
||||
Tạo App
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="builder-layout">
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h2>Thông tin App</h2>
|
||||
</div>
|
||||
</div>
|
||||
<form id="builderForm" class="form-stack" action="/applications" method="post">
|
||||
<label class="form-field">
|
||||
<span>App code</span>
|
||||
<input type="text" name="appCode" required>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>App version</span>
|
||||
<input type="text" name="appVersion" required>
|
||||
</label>
|
||||
<label class="form-field full">
|
||||
<span>App name</span>
|
||||
<input type="text" name="appName" required>
|
||||
</label>
|
||||
<label class="form-field full">
|
||||
<span>Notes</span>
|
||||
<textarea name="notes"></textarea>
|
||||
</label>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="table-panel builder-table">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h2>Chọn package</h2>
|
||||
<p>Có thể dùng chung `.deb` và Docker trong một app.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-filters inline">
|
||||
<label class="filter-field wide">
|
||||
<span>Search</span>
|
||||
<input type="search" placeholder="Tìm package..." data-table-search="builderPackagesTable">
|
||||
</label>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table id="builderPackagesTable" class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Use</th>
|
||||
<th>Package</th>
|
||||
<th>Type</th>
|
||||
<th>Version</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (packages.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="5" class="table-empty">Chưa có package để đóng gói. Hãy upload package trước.</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
<% packages.forEach((item, index) => { %>
|
||||
<tr data-search="<%= `${item.name} ${item.code} ${item.latestVersion}`.toLowerCase() %>">
|
||||
<td>
|
||||
<input class="checkbox" form="builderForm" type="checkbox" name="packageIds" value="<%= item.id %>" <%= index < 3 ? 'checked' : '' %> aria-label="Chọn <%= item.name %>">
|
||||
</td>
|
||||
<td>
|
||||
<strong><%= item.name %></strong>
|
||||
<span class="table-subtitle"><%= item.code %></span>
|
||||
</td>
|
||||
<td><span class="badge <%= helpers.packageTypeClass(item.type) %>"><%= helpers.packageTypeLabel(item.type) %></span></td>
|
||||
<td>
|
||||
<select class="mini-select" form="builderForm" name="version_<%= item.id %>" aria-label="Version của <%= item.name %>">
|
||||
<% item.versions.forEach((version) => { %>
|
||||
<option value="<%= version.id %>"><%= version.version %></option>
|
||||
<% }) %>
|
||||
</select>
|
||||
</td>
|
||||
<td><span class="badge <%= helpers.statusClass(item.status) %>"><%= item.status %></span></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<%- include('partials/page-end') %>
|
||||
54
web-server/views/confirm-email-sent.ejs
Normal file
54
web-server/views/confirm-email-sent.ejs
Normal file
@@ -0,0 +1,54 @@
|
||||
<!doctype html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><%= title %> | Robot Installer</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/vendor/notiflix/notiflix-<%= notiflixVersion %>.min.css">
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
</head>
|
||||
<body class="auth-shell" <% if (notice) { %>data-notice-type="<%= notice.type %>" data-notice="<%= notice.message %>"<% } %>>
|
||||
<main class="auth-page">
|
||||
<section class="auth-panel">
|
||||
<div class="auth-brand">
|
||||
<div class="brand-mark">
|
||||
<span class="material-symbols-outlined">precision_manufacturing</span>
|
||||
</div>
|
||||
<div class="brand-copy">
|
||||
<strong>Robot Installer</strong>
|
||||
<span>Email Confirmation</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="auth-confirm-icon">
|
||||
<span class="material-symbols-outlined">mark_email_unread</span>
|
||||
</div>
|
||||
|
||||
<div class="auth-heading">
|
||||
<h1>Kiểm tra email</h1>
|
||||
<p>Chúng tôi đã gửi link xác nhận tới email đăng ký. Tài khoản chỉ được kích hoạt sau khi bạn bấm link confirm.</p>
|
||||
</div>
|
||||
|
||||
<form class="auth-form" method="post" action="/resend-confirmation">
|
||||
<label class="form-field">
|
||||
<span>Email đăng ký</span>
|
||||
<input type="email" name="email" value="<%= email %>" autocomplete="email" required>
|
||||
</label>
|
||||
<button class="btn btn-secondary auth-submit" type="submit">
|
||||
<span class="material-symbols-outlined">forward_to_inbox</span>
|
||||
Gửi lại email xác nhận
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="auth-switch">Đã xác nhận? <a href="/login">Đăng nhập</a></p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/vendor/notiflix/notiflix-<%= notiflixVersion %>.min.js"></script>
|
||||
<script src="/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
122
web-server/views/dashboard.ejs
Normal file
122
web-server/views/dashboard.ejs
Normal file
@@ -0,0 +1,122 @@
|
||||
<%- include('partials/page-start') %>
|
||||
|
||||
<section class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>Tổng quan</h1>
|
||||
<p>Theo dõi nhanh package, version mới nhất và các app đang được đóng gói.</p>
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
<button class="btn btn-secondary" type="button" data-modal-open="uploadPackageModal">
|
||||
<span class="material-symbols-outlined">upload_file</span>
|
||||
Upload package
|
||||
</button>
|
||||
<a class="btn btn-primary" href="/builder">
|
||||
<span class="material-symbols-outlined">add_box</span>
|
||||
Tạo App
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-stats">
|
||||
<article class="metric-card">
|
||||
<span>Packages</span>
|
||||
<div>
|
||||
<strong><%= stats.totalPackages %></strong>
|
||||
<small><%= stats.activePackages %> active</small>
|
||||
</div>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<span>Versions</span>
|
||||
<div>
|
||||
<strong><%= stats.totalVersions %></strong>
|
||||
<small>latest tracking</small>
|
||||
</div>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<span>Applications</span>
|
||||
<div>
|
||||
<strong><%= stats.totalApplications %></strong>
|
||||
<small><%= stats.releasedApplications %> released</small>
|
||||
</div>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<span>Package types</span>
|
||||
<div>
|
||||
<strong>2</strong>
|
||||
<small>.deb + docker</small>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-grid">
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h2>Package mới cập nhật</h2>
|
||||
<p>Hiển thị version mới nhất cho từng package.</p>
|
||||
</div>
|
||||
<a href="/packages" class="text-link">Xem tất cả</a>
|
||||
</div>
|
||||
<div class="table-wrap compact">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Package</th>
|
||||
<th>Type</th>
|
||||
<th>Latest</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (packages.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="4" class="table-empty">Chưa có package nào. Bấm Upload package để thêm dữ liệu đầu tiên.</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
<% packages.slice(0, 4).forEach((item) => { %>
|
||||
<tr>
|
||||
<td>
|
||||
<a class="table-title" href="/packages/<%= item.id %>"><%= item.name %></a>
|
||||
<span class="table-subtitle"><%= item.code %></span>
|
||||
</td>
|
||||
<td><span class="badge <%= helpers.packageTypeClass(item.type) %>"><%= helpers.packageTypeLabel(item.type) %></span></td>
|
||||
<td><%= item.latestVersion %></td>
|
||||
<td><span class="badge <%= helpers.statusClass(item.status) %>"><%= item.status %></span></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h2>Hoạt động gần đây</h2>
|
||||
<p>Các thay đổi chính trong package/app.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="activity-list">
|
||||
<% if (activity.length === 0) { %>
|
||||
<div class="table-empty">Chưa có hoạt động upload/update package.</div>
|
||||
<% } %>
|
||||
<% activity.forEach((item) => { %>
|
||||
<div class="activity-item">
|
||||
<div class="activity-icon">
|
||||
<span class="material-symbols-outlined"><%= item.icon %></span>
|
||||
</div>
|
||||
<div>
|
||||
<strong><%= item.title %></strong>
|
||||
<span><%= item.detail %></span>
|
||||
</div>
|
||||
<time><%= item.time %></time>
|
||||
</div>
|
||||
<% }) %>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<%- include('partials/package-modal') %>
|
||||
<%- include('partials/page-end') %>
|
||||
15
web-server/views/not-found.ejs
Normal file
15
web-server/views/not-found.ejs
Normal file
@@ -0,0 +1,15 @@
|
||||
<%- include('partials/page-start') %>
|
||||
|
||||
<section class="page center-page">
|
||||
<div class="empty-state">
|
||||
<span class="material-symbols-outlined">search_off</span>
|
||||
<h1>Không tìm thấy dữ liệu</h1>
|
||||
<p>Trang hoặc bản ghi bạn mở không tồn tại trong dữ liệu hiện tại.</p>
|
||||
<a class="btn btn-primary" href="/">
|
||||
<span class="material-symbols-outlined">dashboard</span>
|
||||
Về tổng quan
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<%- include('partials/page-end') %>
|
||||
98
web-server/views/package-detail.ejs
Normal file
98
web-server/views/package-detail.ejs
Normal file
@@ -0,0 +1,98 @@
|
||||
<%- include('partials/page-start') %>
|
||||
|
||||
<section class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="breadcrumb"><a href="/packages">Packages</a><span>/</span><span><%= packageItem.code %></span></div>
|
||||
<h1><%= packageItem.name %></h1>
|
||||
<p><%= packageItem.description %></p>
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
<button class="btn btn-secondary" type="button" data-modal-open="updatePackageModal" data-package-update="<%= packageItem.id %>">
|
||||
<span class="material-symbols-outlined">upgrade</span>
|
||||
Update version
|
||||
</button>
|
||||
<form method="post" action="/packages/<%= packageItem.id %>/delete" data-confirm-submit="Xóa package <%= packageItem.code %>? Thao tác này sẽ xóa mọi version và liên kết app liên quan.">
|
||||
<button class="btn btn-danger" type="submit">
|
||||
<span class="material-symbols-outlined">delete</span>
|
||||
Xóa package
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-grid">
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h2>Thông tin package</h2>
|
||||
<p>Metadata chính dùng cho web server và API.</p>
|
||||
</div>
|
||||
</div>
|
||||
<dl class="detail-list">
|
||||
<div><dt>Code</dt><dd><%= packageItem.code %></dd></div>
|
||||
<div><dt>Type</dt><dd><span class="badge <%= helpers.packageTypeClass(packageItem.type) %>"><%= helpers.packageTypeLabel(packageItem.type) %></span></dd></div>
|
||||
<div><dt>Latest</dt><dd><strong><%= packageItem.latestVersion %></strong></dd></div>
|
||||
<div><dt>Artifact</dt><dd class="mono"><%= packageItem.artifact %></dd></div>
|
||||
<div><dt>Owner</dt><dd><%= packageItem.owner %></dd></div>
|
||||
<div><dt>Status</dt><dd><span class="badge <%= helpers.statusClass(packageItem.status) %>"><%= packageItem.status %></span></dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="panel wide-panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h2>Version history</h2>
|
||||
<p>Mỗi version có ngày upload, changelog và trạng thái.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap compact">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Version</th>
|
||||
<th>Release date</th>
|
||||
<th>Uploaded by</th>
|
||||
<th>Size</th>
|
||||
<th>Status</th>
|
||||
<th class="action-col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% packageItem.versions.forEach((version) => { %>
|
||||
<tr>
|
||||
<td>
|
||||
<strong><%= version.version %></strong>
|
||||
<span class="table-subtitle"><%= version.changeLog %></span>
|
||||
</td>
|
||||
<td><%= version.releaseDate %></td>
|
||||
<td><%= version.uploadedBy %></td>
|
||||
<td><%= version.size %></td>
|
||||
<td><span class="badge <%= helpers.statusClass(version.status) %>"><%= version.status %></span></td>
|
||||
<td class="action-col">
|
||||
<div class="action-group">
|
||||
<form method="post" action="/package-versions/<%= version.id %>/latest">
|
||||
<input type="hidden" name="returnTo" value="<%= currentPath %>">
|
||||
<button class="icon-button subtle" type="submit" title="Đặt latest" aria-label="Đặt latest <%= version.version %>" <%= version.status === 'Latest' ? 'disabled' : '' %>>
|
||||
<span class="material-symbols-outlined">stars</span>
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" action="/package-versions/<%= version.id %>/delete" data-confirm-submit="Xóa version <%= version.version %> khỏi package?">
|
||||
<input type="hidden" name="returnTo" value="<%= currentPath %>">
|
||||
<button class="icon-button danger" type="submit" title="Xóa version" aria-label="Xóa version <%= version.version %>">
|
||||
<span class="material-symbols-outlined">delete</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<%- include('partials/update-package-modal') %>
|
||||
<%- include('partials/page-end') %>
|
||||
108
web-server/views/packages.ejs
Normal file
108
web-server/views/packages.ejs
Normal file
@@ -0,0 +1,108 @@
|
||||
<%- include('partials/page-start') %>
|
||||
|
||||
<section class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>Packages</h1>
|
||||
<p>Quản lý package `.deb`, Docker image, version và trạng thái latest.</p>
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
<a class="btn btn-secondary" href="/packages/export.csv">
|
||||
<span class="material-symbols-outlined">download</span>
|
||||
Export
|
||||
</a>
|
||||
<button class="btn btn-primary" type="button" data-modal-open="uploadPackageModal">
|
||||
<span class="material-symbols-outlined">upload_file</span>
|
||||
Upload package
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-filters">
|
||||
<label class="filter-field">
|
||||
<span>Type</span>
|
||||
<select data-filter-select data-filter-column="type" data-filter-table="packagesTable">
|
||||
<option value="">Tất cả</option>
|
||||
<option value="deb">.deb</option>
|
||||
<option value="docker">Docker</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="filter-field">
|
||||
<span>Status</span>
|
||||
<select data-filter-select data-filter-column="status" data-filter-table="packagesTable">
|
||||
<option value="">Tất cả</option>
|
||||
<option value="Active">Active</option>
|
||||
<option value="Archived">Archived</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="filter-field wide">
|
||||
<span>Search</span>
|
||||
<input type="search" placeholder="Tìm theo tên, code, owner..." data-table-search="packagesTable">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<section class="table-panel">
|
||||
<div class="table-wrap">
|
||||
<table id="packagesTable" class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Package</th>
|
||||
<th>Type</th>
|
||||
<th>Latest version</th>
|
||||
<th>Release date</th>
|
||||
<th>Owner</th>
|
||||
<th>Status</th>
|
||||
<th class="action-col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (packages.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="7" class="table-empty">Chưa có package trong database. Bấm Upload package để tạo package đầu tiên.</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
<% packages.forEach((item) => { %>
|
||||
<tr data-search="<%= `${item.name} ${item.code} ${item.owner} ${item.latestVersion}`.toLowerCase() %>" data-type="<%= item.type %>" data-status="<%= item.status %>">
|
||||
<td>
|
||||
<a class="table-title" href="/packages/<%= item.id %>"><%= item.name %></a>
|
||||
<span class="table-subtitle"><%= item.code %></span>
|
||||
</td>
|
||||
<td><span class="badge <%= helpers.packageTypeClass(item.type) %>"><%= helpers.packageTypeLabel(item.type) %></span></td>
|
||||
<td><strong><%= item.latestVersion %></strong></td>
|
||||
<td><%= item.latestReleaseDate %></td>
|
||||
<td><%= item.owner %></td>
|
||||
<td><span class="badge <%= helpers.statusClass(item.status) %>"><%= item.status %></span></td>
|
||||
<td class="action-col">
|
||||
<div class="action-group">
|
||||
<a class="icon-button subtle" href="/packages/<%= item.id %>" title="Xem chi tiết" aria-label="Xem chi tiết <%= item.name %>">
|
||||
<span class="material-symbols-outlined">visibility</span>
|
||||
</a>
|
||||
<button class="icon-button subtle" type="button" title="Update version" data-modal-open="updatePackageModal" data-package-update="<%= item.id %>">
|
||||
<span class="material-symbols-outlined">upgrade</span>
|
||||
</button>
|
||||
<form method="post" action="/packages/<%= item.id %>/delete" data-confirm-submit="Xóa package <%= item.code %>? Thao tác này sẽ xóa cả version và liên kết app liên quan.">
|
||||
<button class="icon-button danger" type="submit" title="Xóa package" aria-label="Xóa package <%= item.name %>">
|
||||
<span class="material-symbols-outlined">delete</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="page-pager">
|
||||
<span>Showing 1-<%= packages.length %> of <%= packages.length %></span>
|
||||
<div>
|
||||
<button type="button" disabled>Prev</button>
|
||||
<span>Page 1 / 1</span>
|
||||
<button type="button" disabled>Next</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<%- include('partials/package-modal') %>
|
||||
<%- include('partials/update-package-modal') %>
|
||||
<%- include('partials/page-end') %>
|
||||
63
web-server/views/partials/edit-app-modal.ejs
Normal file
63
web-server/views/partials/edit-app-modal.ejs
Normal file
@@ -0,0 +1,63 @@
|
||||
<div class="modal-backdrop" id="editAppModal" role="dialog" aria-modal="true" aria-labelledby="editAppTitle">
|
||||
<div class="modal-content wide">
|
||||
<div class="modal-header">
|
||||
<h3 id="editAppTitle">Sửa Application</h3>
|
||||
<button class="icon-button subtle" type="button" aria-label="Đóng modal" data-modal-close>
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<form id="editAppForm" class="modal-form" method="post" action="/applications">
|
||||
<input type="hidden" name="returnTo" value="<%= currentPath || '/applications' %>">
|
||||
<div class="form-grid">
|
||||
<label class="form-field">
|
||||
<span>App code</span>
|
||||
<input type="text" name="appCode" required data-edit-app-field="appCode">
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Version</span>
|
||||
<input type="text" name="appVersion" required data-edit-app-field="appVersion">
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Status</span>
|
||||
<select name="status" data-edit-app-field="status">
|
||||
<option value="Draft">Draft</option>
|
||||
<option value="Released">Released</option>
|
||||
<option value="Archived">Archived</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="form-field full">
|
||||
<span>App name</span>
|
||||
<input type="text" name="appName" required data-edit-app-field="appName">
|
||||
</label>
|
||||
<label class="form-field full">
|
||||
<span>Notes</span>
|
||||
<textarea name="notes" data-edit-app-field="notes"></textarea>
|
||||
</label>
|
||||
</div>
|
||||
<div class="modal-mini-table">
|
||||
<% packages.forEach((item) => { %>
|
||||
<label>
|
||||
<input class="checkbox" type="checkbox" name="packageIds" value="<%= item.id %>" data-edit-app-package="<%= item.id %>">
|
||||
<span>
|
||||
<strong><%= item.name %></strong>
|
||||
<small><%= item.code %></small>
|
||||
</span>
|
||||
<select class="mini-select" name="version_<%= item.id %>" data-edit-app-version="<%= item.id %>">
|
||||
<option value="">Latest/default</option>
|
||||
<% item.versions.forEach((version) => { %>
|
||||
<option value="<%= version.id %>"><%= version.version %></option>
|
||||
<% }) %>
|
||||
</select>
|
||||
</label>
|
||||
<% }) %>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-secondary" type="button" data-modal-close>Hủy</button>
|
||||
<button class="btn btn-primary" type="submit">
|
||||
<span class="material-symbols-outlined">save</span>
|
||||
Lưu
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
78
web-server/views/partials/package-modal.ejs
Normal file
78
web-server/views/partials/package-modal.ejs
Normal file
@@ -0,0 +1,78 @@
|
||||
<div class="modal-backdrop" id="uploadPackageModal" role="dialog" aria-modal="true" aria-labelledby="uploadPackageTitle">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="uploadPackageTitle">Upload package</h3>
|
||||
<button class="icon-button subtle" type="button" aria-label="Đóng modal" data-modal-close>
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<form class="modal-form" action="/packages" method="post" enctype="multipart/form-data">
|
||||
<div class="form-grid">
|
||||
<label class="form-field">
|
||||
<span>Package code</span>
|
||||
<input type="text" name="packageCode" placeholder="NAV-STACK" required>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Package type</span>
|
||||
<select name="packageType">
|
||||
<option value="deb">.deb</option>
|
||||
<option value="docker">Docker</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="form-field full">
|
||||
<span>Package name</span>
|
||||
<input type="text" name="packageName" placeholder="Navigation Stack" required>
|
||||
</label>
|
||||
<label class="form-field full">
|
||||
<span>Description</span>
|
||||
<input type="text" name="description" placeholder="Mô tả ngắn về package">
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Version</span>
|
||||
<input type="text" name="version" placeholder="1.0.0" required>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Release date</span>
|
||||
<input type="date" name="releaseDate" value="2026-05-19">
|
||||
</label>
|
||||
<div class="form-field full">
|
||||
<span>Package file</span>
|
||||
<div class="file-dropzone" data-file-dropzone>
|
||||
<input class="file-input" type="file" name="packageFile" accept=".deb,.tar,.tar.gz,.tgz,.zip,.gz" data-file-input>
|
||||
<div class="file-dropzone-content">
|
||||
<span class="material-symbols-outlined">upload_file</span>
|
||||
<strong>Kéo file vào đây hoặc chọn từ máy</strong>
|
||||
<small>Hỗ trợ .deb, .tar, .tgz, .zip cho package hoặc Docker image export</small>
|
||||
<button class="btn btn-secondary" type="button" data-file-browse>
|
||||
<span class="material-symbols-outlined">attach_file</span>
|
||||
Chọn file
|
||||
</button>
|
||||
</div>
|
||||
<div class="file-preview" data-file-preview hidden>
|
||||
<span class="material-symbols-outlined">draft</span>
|
||||
<div>
|
||||
<strong data-file-name>Chưa chọn file</strong>
|
||||
<small data-file-meta></small>
|
||||
</div>
|
||||
<button class="icon-button subtle" type="button" title="Bỏ file" aria-label="Bỏ file đã chọn" data-file-clear>
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label class="form-field full">
|
||||
<span>Docker image/tag</span>
|
||||
<input type="text" name="dockerImage" placeholder="registry.local/robot/fleet-agent:1.9.0">
|
||||
</label>
|
||||
<label class="form-field full">
|
||||
<span>Change log</span>
|
||||
<textarea name="changeLog" placeholder="Ghi chú thay đổi"></textarea>
|
||||
</label>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-secondary" type="button" data-modal-close>Hủy</button>
|
||||
<button class="btn btn-primary" type="submit">Upload</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
61
web-server/views/partials/page-end.ejs
Normal file
61
web-server/views/partials/page-end.ejs
Normal file
@@ -0,0 +1,61 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<% if (currentUser && currentUser.role === 'User') { %>
|
||||
<div id="profileModal" class="modal-backdrop" role="dialog" aria-modal="true" aria-labelledby="profileModalTitle">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="profileModalTitle">Thông tin cá nhân</h3>
|
||||
<button class="icon-button subtle" type="button" data-modal-close aria-label="Đóng">
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<form class="modal-form" method="post" action="/profile" data-profile-form>
|
||||
<input type="hidden" name="returnTo" value="<%= currentPath || '/' %>">
|
||||
<div class="profile-summary">
|
||||
<div class="profile-avatar"><%= currentUser.name.charAt(0) %></div>
|
||||
<div>
|
||||
<strong><%= currentUser.name %></strong>
|
||||
<span><%= currentUser.username %></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-stack">
|
||||
<label class="form-field">
|
||||
<span>Fullname</span>
|
||||
<input type="text" name="fullName" value="<%= currentUser.fullName || '' %>" autocomplete="name">
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Email mới</span>
|
||||
<input type="email" name="email" value="<%= currentUser.email || '' %>" autocomplete="email" required data-profile-email>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Confirm email mới</span>
|
||||
<input type="email" name="confirmEmail" value="<%= currentUser.email || '' %>" autocomplete="email" required data-profile-confirm-email>
|
||||
<small class="field-feedback" data-profile-feedback="email"></small>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Mật khẩu mới</span>
|
||||
<input type="password" name="newPassword" minlength="8" autocomplete="new-password" data-profile-new-password>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Xác nhận mật khẩu mới</span>
|
||||
<input type="password" name="confirmPassword" minlength="8" autocomplete="new-password" data-profile-confirm-password>
|
||||
<small class="field-feedback" data-profile-feedback="password"></small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-secondary" type="button" data-modal-close>Hủy</button>
|
||||
<button class="btn btn-primary" type="submit">
|
||||
<span class="material-symbols-outlined">save</span>
|
||||
Lưu
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<script src="/vendor/notiflix/notiflix-<%= notiflixVersion %>.min.js"></script>
|
||||
<script src="/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
87
web-server/views/partials/page-start.ejs
Normal file
87
web-server/views/partials/page-start.ejs
Normal file
@@ -0,0 +1,87 @@
|
||||
<!doctype html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><%= title %> | Robot Installer</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/vendor/notiflix/notiflix-<%= notiflixVersion %>.min.css">
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
</head>
|
||||
<body class="app-shell" <% if (notice) { %>data-notice-type="<%= notice.type %>" data-notice="<%= notice.message %>"<% } %>>
|
||||
<aside id="appSidebar" class="sidebar" aria-label="Main navigation">
|
||||
<div class="brand-block">
|
||||
<div class="brand-mark">
|
||||
<span class="material-symbols-outlined">precision_manufacturing</span>
|
||||
</div>
|
||||
<div class="brand-copy">
|
||||
<strong>Robot Installer</strong>
|
||||
<span>Package Console</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="nav-section" aria-label="Workspace">
|
||||
<span class="nav-label">Workspace</span>
|
||||
<% navItems.forEach((item) => { %>
|
||||
<a href="<%= item.href %>" class="nav-item <%= active === item.id ? 'active' : '' %>">
|
||||
<span class="material-symbols-outlined"><%= item.icon %></span>
|
||||
<span><%= item.label %></span>
|
||||
</a>
|
||||
<% }) %>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-status">
|
||||
<span class="status-dot"></span>
|
||||
<div>
|
||||
<strong>SQL Server</strong>
|
||||
<span>172.20.235.176</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<button id="sidebarBackdrop" type="button" aria-label="Đóng menu"></button>
|
||||
|
||||
<main id="appMain" class="main-shell">
|
||||
<header class="topbar">
|
||||
<div class="topbar-left">
|
||||
<button id="mobileMenuBtn" class="icon-button" type="button" aria-label="Mở menu" aria-expanded="false">
|
||||
<span class="material-symbols-outlined">menu</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="topbar-actions">
|
||||
<button class="icon-button" type="button" title="Đồng bộ dữ liệu" data-refresh-page>
|
||||
<span class="material-symbols-outlined">sync</span>
|
||||
</button>
|
||||
<button class="icon-button" type="button" title="Thông báo" data-toast="Chưa có thông báo mới">
|
||||
<span class="material-symbols-outlined">notifications</span>
|
||||
</button>
|
||||
<% if (currentUser.role === 'User') { %>
|
||||
<button class="profile-chip profile-chip-button" type="button" title="Cập nhật thông tin cá nhân" aria-label="Cập nhật thông tin cá nhân" data-modal-open="profileModal">
|
||||
<span class="profile-avatar"><%= currentUser.name.charAt(0) %></span>
|
||||
<span class="profile-meta">
|
||||
<strong><%= currentUser.name %></strong>
|
||||
<span><%= currentUser.role %></span>
|
||||
</span>
|
||||
</button>
|
||||
<% } else { %>
|
||||
<div class="profile-chip">
|
||||
<span class="profile-avatar"><%= currentUser.name.charAt(0) %></span>
|
||||
<span class="profile-meta">
|
||||
<strong><%= currentUser.name %></strong>
|
||||
<span><%= currentUser.role %></span>
|
||||
</span>
|
||||
</div>
|
||||
<% } %>
|
||||
<form class="logout-form" method="post" action="/logout">
|
||||
<button class="icon-button" type="submit" title="Đăng xuất" aria-label="Đăng xuất">
|
||||
<span class="material-symbols-outlined">logout</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="mainContent" class="main-content">
|
||||
67
web-server/views/partials/update-package-modal.ejs
Normal file
67
web-server/views/partials/update-package-modal.ejs
Normal file
@@ -0,0 +1,67 @@
|
||||
<div class="modal-backdrop" id="updatePackageModal" role="dialog" aria-modal="true" aria-labelledby="updatePackageTitle">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="updatePackageTitle">Update package version</h3>
|
||||
<button class="icon-button subtle" type="button" aria-label="Đóng modal" data-modal-close>
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<form class="modal-form" action="/package-versions" method="post" enctype="multipart/form-data">
|
||||
<div class="form-grid">
|
||||
<label class="form-field full">
|
||||
<span>Package</span>
|
||||
<select name="packageId" required>
|
||||
<% packages.forEach((item) => { %>
|
||||
<option value="<%= item.id %>" <%= typeof packageItem !== 'undefined' && packageItem.id === item.id ? 'selected' : '' %>><%= item.code %> - <%= item.name %></option>
|
||||
<% }) %>
|
||||
</select>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>New version</span>
|
||||
<input type="text" name="version" placeholder="2.5.0" required>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Release date</span>
|
||||
<input type="date" name="releaseDate" value="2026-05-19">
|
||||
</label>
|
||||
<div class="form-field full">
|
||||
<span>Package file</span>
|
||||
<div class="file-dropzone" data-file-dropzone>
|
||||
<input class="file-input" type="file" name="packageFile" accept=".deb,.tar,.tar.gz,.tgz,.zip,.gz" data-file-input>
|
||||
<div class="file-dropzone-content">
|
||||
<span class="material-symbols-outlined">upload_file</span>
|
||||
<strong>Kéo version mới vào đây hoặc chọn file</strong>
|
||||
<small>File .deb hoặc archive Docker export đều dùng được</small>
|
||||
<button class="btn btn-secondary" type="button" data-file-browse>
|
||||
<span class="material-symbols-outlined">attach_file</span>
|
||||
Chọn file
|
||||
</button>
|
||||
</div>
|
||||
<div class="file-preview" data-file-preview hidden>
|
||||
<span class="material-symbols-outlined">draft</span>
|
||||
<div>
|
||||
<strong data-file-name>Chưa chọn file</strong>
|
||||
<small data-file-meta></small>
|
||||
</div>
|
||||
<button class="icon-button subtle" type="button" title="Bỏ file" aria-label="Bỏ file đã chọn" data-file-clear>
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label class="form-field full">
|
||||
<span>Docker image/tag</span>
|
||||
<input type="text" name="dockerImage" placeholder="registry.local/robot/fleet-agent:2.0.0">
|
||||
</label>
|
||||
<label class="form-field full">
|
||||
<span>Change log</span>
|
||||
<textarea name="changeLog" placeholder="Mô tả thay đổi trong version này"></textarea>
|
||||
</label>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-secondary" type="button" data-modal-close>Hủy</button>
|
||||
<button class="btn btn-primary" type="submit">Cập nhật</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
275
web-server/views/users.ejs
Normal file
275
web-server/views/users.ejs
Normal file
@@ -0,0 +1,275 @@
|
||||
<%- include('partials/page-start') %>
|
||||
|
||||
<section class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>Users</h1>
|
||||
<p>Quản lý tài khoản đăng nhập, quyền Admin/User và trạng thái hoạt động.</p>
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
<span class="badge badge-info"><%= users.length %> users</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="users-layout">
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h2>Tạo user mới</h2>
|
||||
</div>
|
||||
</div>
|
||||
<form class="user-create-form" method="post" action="/users">
|
||||
<div class="form-stack">
|
||||
<label class="form-field">
|
||||
<span>Username</span>
|
||||
<input type="text" name="username" autocomplete="off" required>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Họ tên</span>
|
||||
<input type="text" name="fullName" autocomplete="off">
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Email</span>
|
||||
<input type="email" name="email" autocomplete="off" required>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Role</span>
|
||||
<select name="role">
|
||||
<option value="User">User</option>
|
||||
<option value="Admin">Admin</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="form-field full">
|
||||
<span>Mật khẩu tạm</span>
|
||||
<input type="password" name="password" minlength="8" autocomplete="new-password" required>
|
||||
</label>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-primary" type="submit">
|
||||
<span class="material-symbols-outlined">person_add</span>
|
||||
Tạo user
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="table-panel">
|
||||
<div class="page-filters inline">
|
||||
<label class="filter-field">
|
||||
<span>Role</span>
|
||||
<select data-filter-select data-filter-column="role" data-filter-table="usersTable">
|
||||
<option value="">Tất cả</option>
|
||||
<option value="Admin">Admin</option>
|
||||
<option value="User">User</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="filter-field">
|
||||
<span>Status</span>
|
||||
<select data-filter-select data-filter-column="status" data-filter-table="usersTable">
|
||||
<option value="">Tất cả</option>
|
||||
<option value="Active">Active</option>
|
||||
<option value="Inactive">Inactive</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="filter-field wide">
|
||||
<span>Search</span>
|
||||
<input type="search" placeholder="Tìm theo username, email, họ tên..." data-table-search="usersTable">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table id="usersTable" class="data-table users-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
<th>Status</th>
|
||||
<th>Created</th>
|
||||
<th>Owned data</th>
|
||||
<th>Session</th>
|
||||
<th class="action-col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (users.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="8" class="table-empty">Chưa có user nào.</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
<% users.forEach((user) => { %>
|
||||
<tr
|
||||
data-search="<%= `${user.username} ${user.email} ${user.fullName}`.toLowerCase() %>"
|
||||
data-role="<%= user.role %>"
|
||||
data-status="<%= user.status %>"
|
||||
data-user-id="<%= user.id %>"
|
||||
data-user-name="<%= user.name %>"
|
||||
data-user-username="<%= user.username %>"
|
||||
data-user-email="<%= user.email %>"
|
||||
data-user-full-name="<%= user.fullName %>"
|
||||
data-user-role="<%= user.role %>"
|
||||
data-user-status="<%= user.status %>"
|
||||
data-user-active="<%= user.isActive ? 'true' : 'false' %>"
|
||||
data-user-created-at="<%= user.createdAt %>"
|
||||
data-user-updated-at="<%= user.updatedAt %>"
|
||||
data-user-package-count="<%= user.packageCount %>"
|
||||
data-user-application-count="<%= user.applicationCount %>"
|
||||
>
|
||||
<td>
|
||||
<span class="table-title"><%= user.name %></span>
|
||||
<span class="table-subtitle"><%= user.username %></span>
|
||||
</td>
|
||||
<td><%= user.email %></td>
|
||||
<td><span class="badge <%= helpers.statusClass(user.role) %>"><%= user.role %></span></td>
|
||||
<td><span class="badge <%= helpers.statusClass(user.status) %>"><%= user.status %></span></td>
|
||||
<td><%= user.createdAt %></td>
|
||||
<td>
|
||||
<span class="table-subtitle"><%= user.packageCount %> packages</span>
|
||||
<span class="table-subtitle"><%= user.applicationCount %> apps</span>
|
||||
</td>
|
||||
<td>
|
||||
<% if (user.id === currentUser.id) { %>
|
||||
<span class="badge badge-muted">Đang đăng nhập</span>
|
||||
<% } else { %>
|
||||
<span class="table-subtitle">-</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td class="action-col">
|
||||
<div class="user-actions">
|
||||
<button class="icon-button subtle" type="button" title="Xem user" aria-label="Xem user <%= user.username %>" data-user-view>
|
||||
<span class="material-symbols-outlined">visibility</span>
|
||||
</button>
|
||||
<button class="icon-button subtle" type="button" title="Sửa user" aria-label="Sửa user <%= user.username %>" data-user-edit <%= user.id === currentUser.id ? 'data-current-user="true"' : '' %>>
|
||||
<span class="material-symbols-outlined">edit</span>
|
||||
</button>
|
||||
<% if (user.id !== currentUser.id) { %>
|
||||
<form method="post" action="/users/<%= user.id %>/delete" data-confirm-submit="Xóa user <%= user.username %>? Thao tác này không thể hoàn tác.">
|
||||
<button class="icon-button danger" type="submit" title="Xóa user" aria-label="Xóa user <%= user.username %>">
|
||||
<span class="material-symbols-outlined">delete</span>
|
||||
</button>
|
||||
</form>
|
||||
<% } %>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="page-pager">
|
||||
<span>Showing 1-<%= users.length %> of <%= users.length %></span>
|
||||
<div>
|
||||
<button type="button" disabled>Prev</button>
|
||||
<span>Page 1 / 1</span>
|
||||
<button type="button" disabled>Next</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="userDetailModal" class="modal-backdrop" role="dialog" aria-modal="true" aria-labelledby="userDetailTitle">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="userDetailTitle">Thông tin user</h3>
|
||||
<button class="icon-button subtle" type="button" data-modal-close aria-label="Đóng">
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-form">
|
||||
<dl class="detail-list user-detail-list">
|
||||
<div>
|
||||
<dt>Họ tên</dt>
|
||||
<dd data-user-detail="name"></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Username</dt>
|
||||
<dd data-user-detail="username"></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Email</dt>
|
||||
<dd data-user-detail="email"></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Mật khẩu</dt>
|
||||
<dd>Không hiển thị. Có thể đặt lại trong phần Sửa user.</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Role</dt>
|
||||
<dd data-user-detail="role"></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Status</dt>
|
||||
<dd data-user-detail="status"></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Created</dt>
|
||||
<dd data-user-detail="createdAt"></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Updated</dt>
|
||||
<dd data-user-detail="updatedAt"></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Owned data</dt>
|
||||
<dd data-user-detail="ownedData"></dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="editUserModal" class="modal-backdrop" role="dialog" aria-modal="true" aria-labelledby="editUserTitle">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="editUserTitle">Sửa user</h3>
|
||||
<button class="icon-button subtle" type="button" data-modal-close aria-label="Đóng">
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<form id="editUserForm" class="modal-form" method="post" action="/users">
|
||||
<div class="form-stack">
|
||||
<label class="form-field">
|
||||
<span>Username</span>
|
||||
<input type="text" name="username" required data-edit-user-field="username">
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Họ tên</span>
|
||||
<input type="text" name="fullName" data-edit-user-field="fullName">
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Email</span>
|
||||
<input type="email" name="email" required data-edit-user-field="email">
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Role</span>
|
||||
<select name="role" data-edit-user-field="role">
|
||||
<option value="User">User</option>
|
||||
<option value="Admin">Admin</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Mật khẩu mới</span>
|
||||
<input type="password" name="newPassword" minlength="8" autocomplete="new-password" data-edit-user-field="newPassword">
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Xác nhận mật khẩu mới</span>
|
||||
<input type="password" name="confirmPassword" minlength="8" autocomplete="new-password" data-edit-user-field="confirmPassword">
|
||||
</label>
|
||||
<label class="inline-checkbox edit-active-toggle">
|
||||
<input class="checkbox" type="checkbox" name="isActive" data-edit-user-field="isActive">
|
||||
Active
|
||||
</label>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-secondary" type="button" data-modal-close>Hủy</button>
|
||||
<button class="btn btn-primary" type="submit">
|
||||
<span class="material-symbols-outlined">save</span>
|
||||
Lưu
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('partials/page-end') %>
|
||||
Reference in New Issue
Block a user