Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78adc11e1d | ||
|
|
c5512d39d0 | ||
|
|
9fff01a272 |
132
.gitea/workflows/deploy.yml
Normal file
132
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
name: Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: prod-deploy
|
||||||
|
env:
|
||||||
|
DEPLOY_BASE: /opt/ziwei-power
|
||||||
|
REPO_URL: https://qiukai:${{ secrets.DEPLOY_TOKEN }}@git.qiukai.me/qiukai/ziwei-power.git
|
||||||
|
SERVICE_NAME: ziwei-power
|
||||||
|
steps:
|
||||||
|
- name: Clone and deploy
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
RELEASE_ID="${{ github.sha }}"
|
||||||
|
RELEASE_DIR="${DEPLOY_BASE}/releases/${RELEASE_ID}"
|
||||||
|
CLONE_DIR="/tmp/zw-deploy-${RELEASE_ID}"
|
||||||
|
|
||||||
|
echo "=== 1. Clone repository ==="
|
||||||
|
rm -rf "${CLONE_DIR}"
|
||||||
|
git clone --depth 1 --branch main "${REPO_URL}" "${CLONE_DIR}"
|
||||||
|
|
||||||
|
echo "=== 2. Prepare release directory ==="
|
||||||
|
rm -rf "${RELEASE_DIR}"
|
||||||
|
mkdir -p "${RELEASE_DIR}"
|
||||||
|
|
||||||
|
# Copy repo content to release dir (exclude .git, .env, venv, data)
|
||||||
|
rsync -a --exclude='.git' \
|
||||||
|
--exclude='.env' \
|
||||||
|
--exclude='.env.local' \
|
||||||
|
--exclude='.venv' \
|
||||||
|
--exclude='data/' \
|
||||||
|
--exclude='__pycache__' \
|
||||||
|
--exclude='.gitea' \
|
||||||
|
"${CLONE_DIR}/" "${RELEASE_DIR}/"
|
||||||
|
|
||||||
|
echo "=== 2.5 Ensure .env secrets ==="
|
||||||
|
mkdir -p "${DEPLOY_BASE}/shared"
|
||||||
|
SHARED_ENV="${DEPLOY_BASE}/shared/.env"
|
||||||
|
touch "${SHARED_ENV}"
|
||||||
|
ensure_env_var() {
|
||||||
|
key="$1"; val="$2"
|
||||||
|
if [ -z "$val" ]; then
|
||||||
|
echo " ! ${key} 未提供(CI secret 为空),跳过写入"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if ! grep -q "^${key}=" "${SHARED_ENV}"; then
|
||||||
|
echo "${key}=${val}" >> "${SHARED_ENV}"
|
||||||
|
echo " + 已写入 ${key}"
|
||||||
|
else
|
||||||
|
echo " = ${key} 已存在,保留原值"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
ensure_env_var "SECRET_KEY" "${{ secrets.ZW_SECRET_KEY }}"
|
||||||
|
|
||||||
|
echo "=== 3. Link shared resources ==="
|
||||||
|
mkdir -p "${RELEASE_DIR}/data"
|
||||||
|
# .env from shared dir (not in git)
|
||||||
|
ln -sfn "${DEPLOY_BASE}/shared/.env" "${RELEASE_DIR}/.env"
|
||||||
|
|
||||||
|
# Database dir symlink to persist across releases
|
||||||
|
DB_DIR="$HOME/.workbuddy/data/ziwei-power"
|
||||||
|
mkdir -p "${DB_DIR}"
|
||||||
|
ln -sfn "${DB_DIR}" "${RELEASE_DIR}/data"
|
||||||
|
|
||||||
|
echo "=== 4. Setup Python venv ==="
|
||||||
|
cd "${RELEASE_DIR}"
|
||||||
|
python3 -m venv .venv
|
||||||
|
. .venv/bin/activate
|
||||||
|
pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
echo "=== 5. Setup systemd service ==="
|
||||||
|
if ! systemctl is-enabled "${SERVICE_NAME}" >/dev/null 2>&1; then
|
||||||
|
cat > "/etc/systemd/system/${SERVICE_NAME}.service" <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=ziwei-power 日课系统
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=$USER
|
||||||
|
WorkingDirectory=${DEPLOY_BASE}/current
|
||||||
|
ExecStart=${DEPLOY_BASE}/current/.venv/bin/python ${DEPLOY_BASE}/current/app.py
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
Environment=PORT=5058
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable "${SERVICE_NAME}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== 6. Restart service ==="
|
||||||
|
ln -sfn "${RELEASE_DIR}" "${DEPLOY_BASE}/current"
|
||||||
|
systemctl restart "${SERVICE_NAME}"
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
echo "=== 7. Health check ==="
|
||||||
|
for i in 1 2 3 4 5; do
|
||||||
|
if curl -fsS http://127.0.0.1:5058/api/health >/dev/null 2>&1; then
|
||||||
|
echo "Health check passed"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo "Attempt $i: waiting for service..."
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
# Final verify
|
||||||
|
if ! curl -fsS http://127.0.0.1:5058/api/health >/dev/null 2>&1; then
|
||||||
|
echo "ERROR: Health check failed after 5 attempts"
|
||||||
|
echo "Rolling back to previous release..."
|
||||||
|
PREV=$(ls -t "${DEPLOY_BASE}/releases" | sed -n '2p')
|
||||||
|
if [ -n "${PREV}" ]; then
|
||||||
|
ln -sfn "${DEPLOY_BASE}/releases/${PREV}" "${DEPLOY_BASE}/current"
|
||||||
|
systemctl restart "${SERVICE_NAME}"
|
||||||
|
echo "Rolled back to ${PREV}"
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== 8. Cleanup old releases ==="
|
||||||
|
ls -dt "${DEPLOY_BASE}"/releases/*/ | tail -n +6 | xargs -r rm -rf
|
||||||
|
|
||||||
|
echo "=== 9. Cleanup temp ==="
|
||||||
|
rm -rf "${CLONE_DIR}"
|
||||||
|
|
||||||
|
echo "=== Deploy complete: ${RELEASE_ID} ==="
|
||||||
7
app.py
7
app.py
@@ -406,6 +406,13 @@ def api_calendar_sync_all():
|
|||||||
return jsonify({'ok': True, 'data': results, 'saved_days': saved_weeks})
|
return jsonify({'ok': True, 'data': results, 'saved_days': saved_weeks})
|
||||||
|
|
||||||
|
|
||||||
|
# ── 健康检查 ──────────────────────────────────────────
|
||||||
|
|
||||||
|
@app.route('/api/health')
|
||||||
|
def api_health():
|
||||||
|
return jsonify({'ok': True, 'service': 'ziwei-power'})
|
||||||
|
|
||||||
|
|
||||||
# ── 启动 ──────────────────────────────────────────────
|
# ── 启动 ──────────────────────────────────────────────
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -256,9 +256,10 @@
|
|||||||
var eveningItems = [];
|
var eveningItems = [];
|
||||||
var eRows = document.querySelectorAll('#evening-list .evening-row');
|
var eRows = document.querySelectorAll('#evening-list .evening-row');
|
||||||
for (var j=0; j<eRows.length; j++) {
|
for (var j=0; j<eRows.length; j++) {
|
||||||
var inputs = eRows[j].querySelectorAll('input[type="text"]');
|
var inp = eRows[j].querySelector('input[type="text"]');
|
||||||
var mistake = inputs[0] ? inputs[0].value.trim() : '';
|
var noteEl = eRows[j].querySelector('.item-note-val');
|
||||||
var improve = inputs[1] ? inputs[1].value.trim() : '';
|
var mistake = inp ? inp.value.trim() : '';
|
||||||
|
var improve = noteEl ? noteEl.value.trim() : '';
|
||||||
if (mistake || improve) eveningItems.push({ mistake: mistake, improvement: improve });
|
if (mistake || improve) eveningItems.push({ mistake: mistake, improvement: improve });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,7 +354,7 @@
|
|||||||
div.innerHTML = '<span class="drag-handle" title="拖动排序">⋮⋮</span>' +
|
div.innerHTML = '<span class="drag-handle" title="拖动排序">⋮⋮</span>' +
|
||||||
'<span class="idx">' + (idx+1) + '.</span>' +
|
'<span class="idx">' + (idx+1) + '.</span>' +
|
||||||
'<input type="text" value="' + esc(text) + '" placeholder="本周最重要的一件事…">' +
|
'<input type="text" value="' + esc(text) + '" placeholder="本周最重要的一件事…">' +
|
||||||
'<button class="btn-note-toggle' + (hasNote ? ' has-note' : '') + '" onclick="openNoteModal(this)" title="备注">💬</button>' +
|
'<button class="btn-note-toggle' + (hasNote ? ' has-note' : '') + '" onclick="openNoteModal(this)" title="备注"><svg class="icon-sm"><use href="#icon-file-text"/></svg></button>' +
|
||||||
'<button class="btn-del" onclick="this.parentElement.remove();renumberMorning()"><svg class="icon-sm"><use href="#icon-x"/></svg></button>';
|
'<button class="btn-del" onclick="this.parentElement.remove();renumberMorning()"><svg class="icon-sm"><use href="#icon-x"/></svg></button>';
|
||||||
// 隐藏存储备注
|
// 隐藏存储备注
|
||||||
var noteHidden = document.createElement('input');
|
var noteHidden = document.createElement('input');
|
||||||
@@ -372,7 +373,7 @@
|
|||||||
var noteModalRow = null;
|
var noteModalRow = null;
|
||||||
|
|
||||||
window.openNoteModal = function(btn) {
|
window.openNoteModal = function(btn) {
|
||||||
noteModalRow = btn.closest('.item-row');
|
noteModalRow = btn.closest('.item-row') || btn.closest('.evening-row');
|
||||||
var noteEl = noteModalRow.querySelector('.item-note-val');
|
var noteEl = noteModalRow.querySelector('.item-note-val');
|
||||||
var input = noteModalRow.querySelector('input[type="text"]');
|
var input = noteModalRow.querySelector('input[type="text"]');
|
||||||
var title = input ? input.value.trim() || '未命名任务' : '备注';
|
var title = input ? input.value.trim() || '未命名任务' : '备注';
|
||||||
@@ -411,14 +412,22 @@
|
|||||||
var container = document.getElementById('evening-list');
|
var container = document.getElementById('evening-list');
|
||||||
mistake = mistake || '';
|
mistake = mistake || '';
|
||||||
improve = improve || '';
|
improve = improve || '';
|
||||||
|
var hasNote = !!improve;
|
||||||
var div = document.createElement('div');
|
var div = document.createElement('div');
|
||||||
div.className = 'evening-row';
|
div.className = 'evening-row';
|
||||||
div.draggable = true;
|
div.draggable = true;
|
||||||
div.innerHTML =
|
div.innerHTML =
|
||||||
'<span class="drag-handle evening-drag" title="拖动排序">⋮⋮</span>' +
|
'<span class="drag-handle evening-drag" title="拖动排序">⋮⋮</span>' +
|
||||||
'<button class="btn-del" onclick="this.parentElement.remove()"><svg class="icon-sm"><use href="#icon-x"/></svg></button>' +
|
'<button class="btn-del" onclick="this.parentElement.remove()"><svg class="icon-sm"><use href="#icon-x"/></svg></button>' +
|
||||||
|
'<div class="evening-row-main">' +
|
||||||
'<input type="text" value="' + esc(mistake) + '" placeholder="犯的错误…">' +
|
'<input type="text" value="' + esc(mistake) + '" placeholder="犯的错误…">' +
|
||||||
'<input type="text" value="' + esc(improve) + '" placeholder="改进方案…">';
|
'<button class="btn-note-toggle' + (hasNote ? ' has-note' : '') + '" onclick="openNoteModal(this)" title="改进方案"><svg class="icon-sm"><use href="#icon-file-text"/></svg></button>' +
|
||||||
|
'</div>';
|
||||||
|
var noteHidden = document.createElement('input');
|
||||||
|
noteHidden.type = 'hidden';
|
||||||
|
noteHidden.className = 'item-note-val';
|
||||||
|
noteHidden.value = improve;
|
||||||
|
div.appendChild(noteHidden);
|
||||||
div.addEventListener('dragstart', dragStart);
|
div.addEventListener('dragstart', dragStart);
|
||||||
div.addEventListener('dragover', dragOver);
|
div.addEventListener('dragover', dragOver);
|
||||||
div.addEventListener('drop', function(e){ dragDrop.call(this, e, 'evening-list'); });
|
div.addEventListener('drop', function(e){ dragDrop.call(this, e, 'evening-list'); });
|
||||||
|
|||||||
@@ -1469,3 +1469,8 @@ body {
|
|||||||
.item-note { display: none; width: 100%; padding: 6px 10px; border: 1px solid var(--border); border-radius: 6px; font-size: 12px; font-family: inherit; color: var(--text); background: var(--bg); resize: vertical; min-height: 28px; box-sizing: border-box; margin-top: 6px; }
|
.item-note { display: none; width: 100%; padding: 6px 10px; border: 1px solid var(--border); border-radius: 6px; font-size: 12px; font-family: inherit; color: var(--text); background: var(--bg); resize: vertical; min-height: 28px; box-sizing: border-box; margin-top: 6px; }
|
||||||
.item-note:focus { outline: none; border-color: var(--primary); }
|
.item-note:focus { outline: none; border-color: var(--primary); }
|
||||||
.item-row { flex-wrap: wrap; }
|
.item-row { flex-wrap: wrap; }
|
||||||
|
|
||||||
|
/* 责善改过行内布局 */
|
||||||
|
.evening-row-main { display: flex; align-items: center; gap: 8px; width: 100%; }
|
||||||
|
.evening-row-main input[type="text"] { flex: 1; }
|
||||||
|
.evening-row-main .btn-note-toggle { display: inline-flex; }
|
||||||
|
|||||||
@@ -71,4 +71,8 @@
|
|||||||
<symbol id="icon-star" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
<symbol id="icon-star" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.563.563 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.563.563 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z"/>
|
<path d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.563.563 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.563.563 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
<!-- 备注 -->
|
||||||
|
<symbol id="icon-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/>
|
||||||
|
</symbol>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.6 KiB |
Reference in New Issue
Block a user