feat: diary — VPS cloud sync via site_config, API key UI, remove localhost dependency
This commit is contained in:
parent
40dace3ddd
commit
39da164a84
198
index.html
198
index.html
|
|
@ -294,12 +294,36 @@
|
||||||
.diary-textarea::placeholder { color: var(--text3); }
|
.diary-textarea::placeholder { color: var(--text3); }
|
||||||
|
|
||||||
.diary-footer {
|
.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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 10px 16px;
|
padding: 10px 16px;
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
.diary-sync-status {
|
.diary-sync-status {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
|
@ -309,6 +333,23 @@
|
||||||
.diary-sync-status.saved { color: var(--accent); }
|
.diary-sync-status.saved { color: var(--accent); }
|
||||||
.diary-sync-status.pending { color: #FCD34D; }
|
.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) {
|
@media (max-width: 640px) {
|
||||||
.diary-panel { width: 100%; }
|
.diary-panel { width: 100%; }
|
||||||
}
|
}
|
||||||
|
|
@ -432,8 +473,20 @@
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
></textarea>
|
></textarea>
|
||||||
<div class="diary-footer">
|
<div class="diary-footer">
|
||||||
<span class="diary-sync-status" id="diarySyncStatus">---</span>
|
<div class="diary-apikey-row" id="diaryApiKeyRow">
|
||||||
<span style="font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--text3);" id="diaryCharCount">0 chars</span>
|
<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>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
|
@ -526,63 +579,65 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Diary panel ──────────────────────────────────────────
|
// ── 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_LS_KEY = 'posimai-log-diary';
|
const DIARY_POST_URL = 'https://api.soar-enrich.com/brain/api/site/config/diary_content';
|
||||||
const diaryPanel = document.getElementById('diaryPanel');
|
const DIARY_LS_KEY = 'posimai-log-diary';
|
||||||
const diaryBtn = document.getElementById('diaryBtn');
|
const APIKEY_LS_KEY = 'posimai_api_key';
|
||||||
const diaryCloseBtn= document.getElementById('diaryCloseBtn');
|
|
||||||
const diaryTextarea= document.getElementById('diaryTextarea');
|
const diaryPanel = document.getElementById('diaryPanel');
|
||||||
const diaryDot = document.getElementById('diaryDot');
|
const diaryBtn = document.getElementById('diaryBtn');
|
||||||
const diarySyncStatus = document.getElementById('diarySyncStatus');
|
const diaryCloseBtn = document.getElementById('diaryCloseBtn');
|
||||||
const diaryCharCount = document.getElementById('diaryCharCount');
|
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;
|
let saveTimer = null;
|
||||||
|
|
||||||
|
function getApiKey() { return localStorage.getItem(APIKEY_LS_KEY) || ''; }
|
||||||
|
|
||||||
function setDiaryStatus(state, text) {
|
function setDiaryStatus(state, text) {
|
||||||
diarySyncStatus.className = 'diary-sync-status ' + state;
|
diarySyncStatus.className = 'diary-sync-status ' + state;
|
||||||
diarySyncStatus.textContent = text;
|
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 fetchDiary() {
|
||||||
async function checkServer() {
|
const res = await fetch(DIARY_GET_URL, { signal: AbortSignal.timeout(4000) });
|
||||||
try {
|
if (!res.ok) throw new Error('fetch failed');
|
||||||
const res = await fetch(DIARY_URL, { signal: AbortSignal.timeout(800) });
|
const data = await res.json();
|
||||||
if (!res.ok) throw new Error();
|
return data.config?.diary_content || '';
|
||||||
serverOnline = true;
|
}
|
||||||
diaryDot.className = 'diary-status-dot connected';
|
|
||||||
// ローカルに保留中のデータがあればサーバーに送る
|
async function saveDiary(content) {
|
||||||
const local = localStorage.getItem(DIARY_LS_KEY);
|
const key = getApiKey();
|
||||||
if (local !== null) {
|
if (!key) throw Object.assign(new Error('no key'), { code: 'nokey' });
|
||||||
const serverData = await res.json();
|
const res = await fetch(DIARY_POST_URL, {
|
||||||
if (local !== serverData.content) {
|
method: 'POST',
|
||||||
await fetch(DIARY_URL, { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ content: local }) });
|
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + key },
|
||||||
}
|
body: JSON.stringify({ value: content }),
|
||||||
}
|
signal: AbortSignal.timeout(5000)
|
||||||
} catch {
|
});
|
||||||
serverOnline = false;
|
if (!res.ok) throw new Error('save failed');
|
||||||
diaryDot.className = 'diary-status-dot' + (localStorage.getItem(DIARY_LS_KEY) ? ' pending' : '');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// パネルを開くときにコンテンツをロード
|
|
||||||
async function openDiary() {
|
async function openDiary() {
|
||||||
diaryPanel.classList.add('open');
|
diaryPanel.classList.add('open');
|
||||||
diaryBtn.setAttribute('aria-expanded', 'true');
|
diaryBtn.setAttribute('aria-expanded', 'true');
|
||||||
setDiaryStatus('', 'connecting...');
|
diaryApiKeyRow.classList.toggle('visible', !getApiKey());
|
||||||
await checkServer();
|
setDiaryStatus('', 'loading...');
|
||||||
try {
|
try {
|
||||||
if (serverOnline) {
|
const content = await fetchDiary();
|
||||||
const res = await fetch(DIARY_URL, { signal: AbortSignal.timeout(800) });
|
diaryTextarea.value = content;
|
||||||
const { content } = await res.json();
|
localStorage.setItem(DIARY_LS_KEY, content);
|
||||||
diaryTextarea.value = content;
|
setDiaryStatus('saved', 'synced');
|
||||||
localStorage.setItem(DIARY_LS_KEY, content);
|
|
||||||
setDiaryStatus('saved', 'synced');
|
|
||||||
} else {
|
|
||||||
diaryTextarea.value = localStorage.getItem(DIARY_LS_KEY) || '';
|
|
||||||
setDiaryStatus('pending', 'offline — saved locally');
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
diaryTextarea.value = localStorage.getItem(DIARY_LS_KEY) || '';
|
diaryTextarea.value = localStorage.getItem(DIARY_LS_KEY) || '';
|
||||||
setDiaryStatus('pending', 'offline — saved locally');
|
setDiaryStatus('pending', 'offline — saved locally');
|
||||||
|
|
@ -596,7 +651,6 @@
|
||||||
diaryBtn.setAttribute('aria-expanded', 'false');
|
diaryBtn.setAttribute('aria-expanded', 'false');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 入力のたびに debounce 保存 (1s)
|
|
||||||
diaryTextarea.addEventListener('input', () => {
|
diaryTextarea.addEventListener('input', () => {
|
||||||
const content = diaryTextarea.value;
|
const content = diaryTextarea.value;
|
||||||
diaryCharCount.textContent = content.length + ' chars';
|
diaryCharCount.textContent = content.length + ' chars';
|
||||||
|
|
@ -604,23 +658,53 @@
|
||||||
setDiaryStatus('pending', 'saving...');
|
setDiaryStatus('pending', 'saving...');
|
||||||
clearTimeout(saveTimer);
|
clearTimeout(saveTimer);
|
||||||
saveTimer = setTimeout(async () => {
|
saveTimer = setTimeout(async () => {
|
||||||
if (serverOnline) {
|
try {
|
||||||
try {
|
await saveDiary(content);
|
||||||
await fetch(DIARY_URL, { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ content }) });
|
setDiaryStatus('saved', 'saved');
|
||||||
setDiaryStatus('saved', 'saved to diary.md');
|
} catch (e) {
|
||||||
} catch {
|
if (e.code === 'nokey') {
|
||||||
serverOnline = false;
|
setDiaryStatus('', 'set API key to sync');
|
||||||
|
diaryApiKeyRow.classList.add('visible');
|
||||||
|
} else {
|
||||||
setDiaryStatus('pending', 'offline — saved locally');
|
setDiaryStatus('pending', 'offline — saved locally');
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
setDiaryStatus('pending', 'offline — saved locally');
|
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 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);
|
diaryBtn.addEventListener('click', openDiary);
|
||||||
diaryCloseBtn.addEventListener('click', closeDiary);
|
diaryCloseBtn.addEventListener('click', closeDiary);
|
||||||
// overlay クリックでも閉じる
|
|
||||||
document.getElementById('overlay').addEventListener('click', closeDiary);
|
document.getElementById('overlay').addEventListener('click', closeDiary);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue