feat: diary — VPS cloud sync via site_config, API key UI, remove localhost dependency

This commit is contained in:
posimai 2026-04-03 15:53:08 +09:00
parent 40dace3ddd
commit 39da164a84
1 changed files with 141 additions and 57 deletions

View File

@ -294,12 +294,36 @@
.diary-textarea::placeholder { color: var(--text3); }
.diary-footer {
display: flex;
flex-direction: column;
border-top: 1px solid var(--border);
flex-shrink: 0;
}
.diary-apikey-row {
display: none;
align-items: center;
gap: 6px;
padding: 8px 16px;
border-bottom: 1px solid var(--border);
}
.diary-apikey-row.visible { display: flex; }
.diary-apikey-input {
flex: 1;
background: transparent;
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text1);
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
padding: 4px 8px;
outline: none;
}
.diary-apikey-input:focus { border-color: var(--accent); }
.diary-footer-main {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
border-top: 1px solid var(--border);
flex-shrink: 0;
}
.diary-sync-status {
font-family: 'JetBrains Mono', monospace;
@ -309,6 +333,23 @@
.diary-sync-status.saved { color: var(--accent); }
.diary-sync-status.pending { color: #FCD34D; }
.diary-action-btn {
display: none;
align-items: center;
gap: 4px;
padding: 3px 8px;
border-radius: 4px;
border: 1px solid var(--border);
background: transparent;
color: var(--text3);
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
cursor: pointer;
transition: color .2s, border-color .2s;
}
.diary-action-btn:hover { color: var(--text1); border-color: var(--text2); }
.diary-action-btn.visible { display: inline-flex; }
@media (max-width: 640px) {
.diary-panel { width: 100%; }
}
@ -432,9 +473,21 @@
spellcheck="false"
></textarea>
<div class="diary-footer">
<div class="diary-apikey-row" id="diaryApiKeyRow">
<input type="password" class="diary-apikey-input" id="diaryApiKeyInput" placeholder="posimai API key">
<button class="diary-action-btn visible" id="diaryApiKeySave" style="display:inline-flex;">set</button>
</div>
<div class="diary-footer-main">
<span class="diary-sync-status" id="diarySyncStatus">---</span>
<div style="display:flex;align-items:center;gap:8px;">
<button class="diary-action-btn" id="diaryReconnectBtn" aria-label="再試行">
<i data-lucide="refresh-cw" style="width:10px;height:10px;stroke-width:2"></i>
retry
</button>
<span style="font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--text3);" id="diaryCharCount">0 chars</span>
</div>
</div>
</div>
</aside>
<div id="toast" role="status" aria-live="polite"></div>
@ -526,63 +579,65 @@
}
// ── Diary panel ──────────────────────────────────────────
const DIARY_URL = 'http://localhost:2626/diary';
const DIARY_GET_URL = 'https://api.soar-enrich.com/brain/api/site/config/public?user=maita';
const DIARY_POST_URL = 'https://api.soar-enrich.com/brain/api/site/config/diary_content';
const DIARY_LS_KEY = 'posimai-log-diary';
const APIKEY_LS_KEY = 'posimai_api_key';
const diaryPanel = document.getElementById('diaryPanel');
const diaryBtn = document.getElementById('diaryBtn');
const diaryCloseBtn= document.getElementById('diaryCloseBtn');
const diaryTextarea= document.getElementById('diaryTextarea');
const diaryCloseBtn = document.getElementById('diaryCloseBtn');
const diaryTextarea = document.getElementById('diaryTextarea');
const diaryDot = document.getElementById('diaryDot');
const diarySyncStatus = document.getElementById('diarySyncStatus');
const diaryCharCount = document.getElementById('diaryCharCount');
const diaryReconnectBtn = document.getElementById('diaryReconnectBtn');
const diaryApiKeyRow = document.getElementById('diaryApiKeyRow');
const diaryApiKeyInput = document.getElementById('diaryApiKeyInput');
const diaryApiKeySave = document.getElementById('diaryApiKeySave');
let serverOnline = false;
let saveTimer = null;
function getApiKey() { return localStorage.getItem(APIKEY_LS_KEY) || ''; }
function setDiaryStatus(state, text) {
diarySyncStatus.className = 'diary-sync-status ' + state;
diarySyncStatus.textContent = text;
diaryDot.className = 'diary-status-dot ' + (serverOnline ? 'connected' : state === 'pending' ? 'pending' : '');
diaryDot.className = 'diary-status-dot' +
(state === 'saved' ? ' connected' : state === 'pending' ? ' pending' : '');
diaryReconnectBtn.classList.toggle('visible', state === 'error');
if (!getApiKey()) diaryApiKeyRow.classList.add('visible');
}
// サーバー接続確認 & 未同期ローカルデータのプッシュ
async function checkServer() {
try {
const res = await fetch(DIARY_URL, { signal: AbortSignal.timeout(800) });
if (!res.ok) throw new Error();
serverOnline = true;
diaryDot.className = 'diary-status-dot connected';
// ローカルに保留中のデータがあればサーバーに送る
const local = localStorage.getItem(DIARY_LS_KEY);
if (local !== null) {
const serverData = await res.json();
if (local !== serverData.content) {
await fetch(DIARY_URL, { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ content: local }) });
}
}
} catch {
serverOnline = false;
diaryDot.className = 'diary-status-dot' + (localStorage.getItem(DIARY_LS_KEY) ? ' pending' : '');
}
async function fetchDiary() {
const res = await fetch(DIARY_GET_URL, { signal: AbortSignal.timeout(4000) });
if (!res.ok) throw new Error('fetch failed');
const data = await res.json();
return data.config?.diary_content || '';
}
async function saveDiary(content) {
const key = getApiKey();
if (!key) throw Object.assign(new Error('no key'), { code: 'nokey' });
const res = await fetch(DIARY_POST_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + key },
body: JSON.stringify({ value: content }),
signal: AbortSignal.timeout(5000)
});
if (!res.ok) throw new Error('save failed');
}
// パネルを開くときにコンテンツをロード
async function openDiary() {
diaryPanel.classList.add('open');
diaryBtn.setAttribute('aria-expanded', 'true');
setDiaryStatus('', 'connecting...');
await checkServer();
diaryApiKeyRow.classList.toggle('visible', !getApiKey());
setDiaryStatus('', 'loading...');
try {
if (serverOnline) {
const res = await fetch(DIARY_URL, { signal: AbortSignal.timeout(800) });
const { content } = await res.json();
const content = await fetchDiary();
diaryTextarea.value = content;
localStorage.setItem(DIARY_LS_KEY, content);
setDiaryStatus('saved', 'synced');
} else {
diaryTextarea.value = localStorage.getItem(DIARY_LS_KEY) || '';
setDiaryStatus('pending', 'offline — saved locally');
}
} catch {
diaryTextarea.value = localStorage.getItem(DIARY_LS_KEY) || '';
setDiaryStatus('pending', 'offline — saved locally');
@ -596,7 +651,6 @@
diaryBtn.setAttribute('aria-expanded', 'false');
}
// 入力のたびに debounce 保存 (1s)
diaryTextarea.addEventListener('input', () => {
const content = diaryTextarea.value;
diaryCharCount.textContent = content.length + ' chars';
@ -604,23 +658,53 @@
setDiaryStatus('pending', 'saving...');
clearTimeout(saveTimer);
saveTimer = setTimeout(async () => {
if (serverOnline) {
try {
await fetch(DIARY_URL, { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ content }) });
setDiaryStatus('saved', 'saved to diary.md');
} catch {
serverOnline = false;
setDiaryStatus('pending', 'offline — saved locally');
}
await saveDiary(content);
setDiaryStatus('saved', 'saved');
} catch (e) {
if (e.code === 'nokey') {
setDiaryStatus('', 'set API key to sync');
diaryApiKeyRow.classList.add('visible');
} else {
setDiaryStatus('pending', 'offline — saved locally');
}
}
}, 1000);
});
diaryReconnectBtn.addEventListener('click', async () => {
setDiaryStatus('', 'retrying...');
try {
const content = await fetchDiary();
diaryTextarea.value = content;
localStorage.setItem(DIARY_LS_KEY, content);
setDiaryStatus('saved', 'synced');
diaryCharCount.textContent = content.length + ' chars';
} catch {
setDiaryStatus('error', 'still offline');
}
});
diaryApiKeySave.addEventListener('click', async () => {
const val = diaryApiKeyInput.value.trim();
if (!val) return;
localStorage.setItem(APIKEY_LS_KEY, val);
diaryApiKeyInput.value = '';
diaryApiKeyRow.classList.remove('visible');
setDiaryStatus('', 'connecting...');
try {
const content = await fetchDiary();
diaryTextarea.value = content;
localStorage.setItem(DIARY_LS_KEY, content);
setDiaryStatus('saved', 'synced');
diaryCharCount.textContent = content.length + ' chars';
} catch {
setDiaryStatus('pending', 'offline — saved locally');
}
});
diaryBtn.addEventListener('click', openDiary);
diaryCloseBtn.addEventListener('click', closeDiary);
// overlay クリックでも閉じる
document.getElementById('overlay').addEventListener('click', closeDiary);
})();
</script>