Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ jobs:
build:
name: Build ESP32-S3 Firmware
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}

steps:
- name: Checkout repository
Expand Down Expand Up @@ -87,9 +89,9 @@ jobs:
run: |
if [ -f build/project_description.json ]; then
VERSION=$(grep -o '"project_version":[^,]*' build/project_description.json | cut -d'"' -f4)
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "📦 Build version: $VERSION"
fi
echo "version=${VERSION:-0.0.0}" >> $GITHUB_OUTPUT
echo "📦 Build version: ${VERSION:-0.0.0}"

- name: Print build size
run: |
Expand Down Expand Up @@ -118,11 +120,11 @@ jobs:
build/config/sdkconfig.h
retention-days: 7

# 可选:发布版本时创建 Release
# 编译成功后自动创建 Release:tag 推送 或 main 分支推送
release:
name: Create Release
needs: build
if: startsWith(github.ref, 'refs/tags/v')
if: startsWith(github.ref, 'refs/tags/v') || (github.ref == 'refs/heads/main' && github.event_name == 'push')
runs-on: ubuntu-latest
permissions:
contents: write # 创建 Release 需要写权限
Expand All @@ -134,9 +136,24 @@ jobs:
pattern: tianshanos-firmware-*
path: firmware

- name: Check if release already exists (main branch)
if: github.ref == 'refs/heads/main'
id: check
run: |
TAG="v${{ needs.build.outputs.version }}"
if gh release view "$TAG" 2>/dev/null; then
echo "skip=true" >> $GITHUB_OUTPUT
echo "⏭️ Release $TAG already exists, skipping"
else
echo "skip=false" >> $GITHUB_OUTPUT
fi

- name: Create Release
if: startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/main' && steps.check.outputs.skip != 'true')
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || format('v{0}', needs.build.outputs.version) }}
name: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || format('v{0}', needs.build.outputs.version) }}
files: firmware/**/*.bin
generate_release_notes: true
env:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ esptool.py --chip esp32s3 -p /dev/ttyACM0 write_flash \

## 当前状态

**版本**: 0.4.0
**版本**: 0.4.4
**阶段**: Phase 38 完成 - WebUI 多语言支持

### 已完成功能
Expand Down
31 changes: 29 additions & 2 deletions components/ts_security/src/ts_security.c
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,22 @@ esp_err_t ts_security_store_cert(const char *name, ts_cert_type_t type,
return ret;
}

/**
* @brief 驱逐指定 client_id 的所有会话(释放槽位)
* @note 仅内部使用,在 create_session 槽位满时按同用户驱逐
*/
static void destroy_sessions_by_client(const char *client_id)
{
if (!client_id || client_id[0] == '\0') return;
for (int i = 0; i < MAX_SESSIONS; i++) {
if (s_sessions[i].active &&
strcmp(s_sessions[i].session.client_id, client_id) == 0) {
s_sessions[i].active = false;
TS_LOGI(TAG, "Evicted session for client: %s", client_id);
}
}
}

esp_err_t ts_security_create_session(const char *client_id, ts_perm_level_t level,
uint32_t *session_id)
{
Expand All @@ -181,8 +197,19 @@ esp_err_t ts_security_create_session(const char *client_id, ts_perm_level_t leve
}

if (slot < 0) {
TS_LOGW(TAG, "No free session slots");
return ESP_ERR_NO_MEM;
if (client_id && client_id[0] != '\0') {
destroy_sessions_by_client(client_id);
for (int i = 0; i < MAX_SESSIONS; i++) {
if (!s_sessions[i].active) {
slot = i;
break;
}
}
}
if (slot < 0) {
TS_LOGW(TAG, "No free session slots");
return ESP_ERR_NO_MEM;
}
}

// Generate session ID
Expand Down
5 changes: 5 additions & 0 deletions components/ts_webui/src/ts_webui_api.c
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,11 @@ static esp_err_t login_handler(ts_http_request_t *req, void *user_data)
cJSON_AddNumberToObject(data, "session_id", session_id);
cJSON_AddStringToObject(data, "username", username_copy);
cJSON_AddStringToObject(data, "level", level_str);
#ifdef CONFIG_TS_SECURITY_TOKEN_EXPIRE_SEC
cJSON_AddNumberToObject(data, "expires_in", CONFIG_TS_SECURITY_TOKEN_EXPIRE_SEC);
#else
cJSON_AddNumberToObject(data, "expires_in", 86400); /* 24 hours */
#endif
cJSON_AddBoolToObject(data, "password_changed", password_changed);
cJSON_AddItemToObject(response, "data", data);

Expand Down
14 changes: 14 additions & 0 deletions components/ts_webui/web/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -7591,6 +7591,20 @@ button.btn-gray:hover,
box-shadow: 0 2px 8px var(--blue-100);
}

/* 长按拖拽排序:等待中、拖拽中、插入位置指示 */
.drag-pending {
outline: 2px solid var(--blue-500) !important;
outline-offset: 2px;
opacity: 0.8;
transition: opacity 3s linear;
}
.drag-active-source {
opacity: 0.25 !important;
}
.drag-over-before {
border-top: 2px solid var(--blue-500) !important;
}

.dw-card-header {
display: flex;
justify-content: space-between;
Expand Down
3 changes: 3 additions & 0 deletions components/ts_webui/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,8 @@ <h2><span data-i18n="system.shutdownSettings">电压保护设置</span></h2>
var nameEl = document.getElementById('lang-name');
if (nameEl) nameEl.textContent = cur === 'zh-CN' ? '中文' : 'EN';
i18n.translateDOM();
/* translateDOM 会覆盖 #user-name 为「未登录」,需在之后恢复已登录状态 */
if (typeof updateAuthUI === 'function') setTimeout(updateAuthUI, 0);
};
document.head.appendChild(s);
})();
Expand All @@ -379,6 +381,7 @@ <h2><span data-i18n="system.shutdownSettings">电压保护设置</span></h2>
<script defer src="/js/api.js"></script>
<script defer src="/js/router.js"></script>
<script defer src="/js/terminal.js"></script>
<script defer src="/js/dragSort.js"></script>
<script defer src="/js/app.js"></script>
</body>
</html>
9 changes: 7 additions & 2 deletions components/ts_webui/web/js/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,10 @@ class TianShanAPI {
// 尝试从 localStorage 恢复
const savedToken = localStorage.getItem('ts_token');
const expires = localStorage.getItem('ts_expires');
if (savedToken && expires && Date.now() < parseInt(expires)) {
const parsed = expires ? parseInt(expires) : NaN;
const now = Date.now();
const valid = savedToken && expires && now < parsed;
if (valid) {
this.token = savedToken;
this.username = localStorage.getItem('ts_username');
this.level = localStorage.getItem('ts_level');
Expand All @@ -232,7 +235,9 @@ class TianShanAPI {
}
// 检查是否过期
const expires = localStorage.getItem('ts_expires');
if (expires && Date.now() >= parseInt(expires)) {
const parsed = expires ? parseInt(expires) : NaN;
const expired = expires && Date.now() >= parsed;
if (expired) {
this.logout(); // 清理过期的 token
return false;
}
Expand Down
69 changes: 66 additions & 3 deletions components/ts_webui/web/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ document.addEventListener('DOMContentLoaded', () => {

// 语言切换时重新渲染当前页,使主内容使用新语言;下一帧恢复右上角登录态(避免 translateDOM 覆盖 #user-name)
window.addEventListener('languageChanged', () => {
if (typeof stopSystemPageTimers === 'function') {
stopSystemPageTimers();
}
const loader = router.getCurrentLoader();
if (loader) loader();
setTimeout(() => updateAuthUI(), 0);
Expand All @@ -194,7 +197,6 @@ document.addEventListener('DOMContentLoaded', () => {
function updateAuthUI() {
const loginBtn = document.getElementById('login-btn');
const userName = document.getElementById('user-name');

if (api.isLoggedIn()) {
const username = api.getUsername();
const level = api.getLevel();
Expand Down Expand Up @@ -771,6 +773,39 @@ async function loadSystemPage() {

// 启动设备状态实时监控
startDeviceStateMonitor();

// 初始化设备面板长按拖拽排序
const widgetGrid = document.getElementById('data-widgets-grid');
if (widgetGrid && typeof initLongPressDragSort === 'function') {
const { destroy: destroyWidgetSort } = initLongPressDragSort(widgetGrid, {
itemSelector: '.dw-card',
idAttribute: 'data-widget-id',
holdMs: 1500,
onReorder(oldIdx, newIdx) {
const [moved] = dataWidgets.splice(oldIdx, 1);
dataWidgets.splice(newIdx, 0, moved);
saveDataWidgets();
renderDataWidgets();
}
});
window._destroyWidgetSort = destroyWidgetSort;
}
const quickGrid = document.getElementById('quick-actions-grid');
if (quickGrid && typeof initLongPressDragSort === 'function') {
const { destroy: destroyQuickSort } = initLongPressDragSort(quickGrid, {
itemSelector: '.quick-action-card',
idAttribute: 'data-rule-id',
holdMs: 1500,
updateDOM: true,
onReorder() {
const newOrder = [...quickGrid.querySelectorAll('.quick-action-card')]
.map(el => el.getAttribute('data-rule-id')).filter(Boolean);
try { localStorage.setItem('quick_actions_order', JSON.stringify(newOrder)); }
catch (e) { /* 静默忽略 */ }
}
});
window._destroyQuickSort = destroyQuickSort;
}
}

// 单次刷新(初始加载);refreshSystemPage 为别名,供时区/服务操作等调用
Expand Down Expand Up @@ -4017,6 +4052,26 @@ async function refreshQuickActions() {
const allRules = result.data.rules;
const manualRules = allRules.filter(r => r.enabled && r.manual_trigger);

// 按 localStorage 保存的顺序排列快捷操作
let savedOrder = [];
try {
const raw = localStorage.getItem('quick_actions_order');
if (raw) {
savedOrder = JSON.parse(raw);
if (!Array.isArray(savedOrder)) savedOrder = [];
}
} catch (e) { savedOrder = []; }
if (savedOrder.length > 0) {
manualRules.sort((a, b) => {
const ia = savedOrder.indexOf(a.id);
const ib = savedOrder.indexOf(b.id);
if (ia === -1 && ib === -1) return 0;
if (ia === -1) return 1;
if (ib === -1) return -1;
return ia - ib;
});
}

if (manualRules.length > 0) {
// 串行检查每个规则的 nohup 状态并生成卡片,避免多路 ssh.exec 并发导致后端串行/覆盖、结果错位
const cardsHtml = [];
Expand Down Expand Up @@ -4194,12 +4249,20 @@ let _quickActionTriggerCooldownUntil = 0;
let _quickActionLastTriggeredId = '';
let quickActionsTimeoutId = null; // 用于导航时取消,避免 quick-actions-grid not found

/** 供 router 在页面切换时取消快捷操作定时器 */
/** 供 router 在页面切换时取消快捷操作定时器,并销毁拖拽排序(防止 ghost 残留) */
window.stopSystemPageTimers = function() {
if (quickActionsTimeoutId) {
clearTimeout(quickActionsTimeoutId);
quickActionsTimeoutId = null;
}
if (typeof window._destroyWidgetSort === 'function') {
window._destroyWidgetSort();
window._destroyWidgetSort = null;
}
if (typeof window._destroyQuickSort === 'function') {
window._destroyQuickSort();
window._destroyQuickSort = null;
}
};

/**
Expand Down Expand Up @@ -7673,7 +7736,7 @@ async function loadFilesPage() {

<!-- 重命名对话框 -->
<div id="rename-modal" class="modal hidden">
<div class="modal-content">
<div class="modal-content" style="max-width:420px">
<h2>${t('files.renameTitle')}</h2>
<div class="form-group">
<label>${t('files.newName')}</label>
Expand Down
Loading
Loading