fix: Pulse UPSERT COALESCE — prevent partial POST from wiping other metrics
ON CONFLICT DO UPDATE now uses COALESCE($3, pulse_log.mood) etc.
so sending only {mood:3} no longer sets energy/focus to NULL.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
db5b3a3961
commit
e7ccd829f6
65
server.js
65
server.js
|
|
@ -147,7 +147,6 @@ async function fetchMeta(url) {
|
|||
const title = og('og:title') || doc.querySelector('title')?.text || url;
|
||||
const desc = og('og:description') || meta('description') || '';
|
||||
const img = og('og:image') || '';
|
||||
let host; // Declare host here for broader scope
|
||||
|
||||
// Google Favicon API(優先)→ favicon.ico(フォールバック)
|
||||
const faviconUrl = `https://www.google.com/s2/favicons?domain=${new URL(url).hostname}&sz=32`;
|
||||
|
|
@ -266,7 +265,7 @@ ${smartExtract(fullText || '', 5000)}
|
|||
console.error('[Gemini] Raw response:', result.response.text());
|
||||
}
|
||||
return {
|
||||
summary: `⚠️ AI分析失敗: ${String(e)}`,
|
||||
summary: 'AI分析に失敗しました。しばらく後にお試しください。',
|
||||
topics: ['その他'],
|
||||
readingTime: 3
|
||||
};
|
||||
|
|
@ -441,50 +440,7 @@ async function initDB() {
|
|||
`ALTER TABLE site_config ADD COLUMN IF NOT EXISTS user_id VARCHAR(50) NOT NULL DEFAULT 'maita'`,
|
||||
`ALTER TABLE site_config DROP CONSTRAINT IF EXISTS site_config_pkey`,
|
||||
`ALTER TABLE site_config ADD PRIMARY KEY (user_id, key)`,
|
||||
// together テーブルをクリーンリセット(古いスキーマを完全に作り直す)
|
||||
`DROP TABLE IF EXISTS together_comments, together_reactions, together_shares, together_members, together_groups CASCADE`,
|
||||
`CREATE TABLE together_groups (
|
||||
id SERIAL PRIMARY KEY,
|
||||
invite_code VARCHAR(8) UNIQUE NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
)`,
|
||||
`CREATE TABLE together_members (
|
||||
group_id INTEGER REFERENCES together_groups(id) ON DELETE CASCADE,
|
||||
username VARCHAR(50) NOT NULL,
|
||||
joined_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
PRIMARY KEY (group_id, username)
|
||||
)`,
|
||||
`CREATE TABLE together_shares (
|
||||
id SERIAL PRIMARY KEY,
|
||||
group_id INTEGER REFERENCES together_groups(id) ON DELETE CASCADE,
|
||||
shared_by VARCHAR(50) NOT NULL,
|
||||
url TEXT,
|
||||
title TEXT,
|
||||
message TEXT NOT NULL DEFAULT '',
|
||||
og_image TEXT,
|
||||
tags TEXT[] DEFAULT '{}',
|
||||
full_content TEXT,
|
||||
summary TEXT,
|
||||
archive_status VARCHAR(10) NOT NULL DEFAULT 'pending',
|
||||
shared_at TIMESTAMPTZ DEFAULT NOW()
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_together_shares_group ON together_shares(group_id, shared_at DESC)`,
|
||||
`CREATE TABLE together_reactions (
|
||||
share_id INTEGER REFERENCES together_shares(id) ON DELETE CASCADE,
|
||||
username VARCHAR(50) NOT NULL,
|
||||
type VARCHAR(20) NOT NULL DEFAULT 'like',
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
PRIMARY KEY (share_id, username, type)
|
||||
)`,
|
||||
`CREATE TABLE together_comments (
|
||||
id SERIAL PRIMARY KEY,
|
||||
share_id INTEGER REFERENCES together_shares(id) ON DELETE CASCADE,
|
||||
username VARCHAR(50) NOT NULL,
|
||||
body TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_together_comments_share ON together_comments(share_id, created_at)`,
|
||||
// together スキーマは schema 配列の CREATE TABLE IF NOT EXISTS で管理
|
||||
];
|
||||
for (const sql of migrations) {
|
||||
await pool.query(sql).catch(e => console.warn('[DB] Migration warning:', e.message));
|
||||
|
|
@ -728,7 +684,7 @@ function buildRouter() {
|
|||
|
||||
// GET /api/history - 履歴取得
|
||||
r.get('/history', authMiddleware, async (req, res) => {
|
||||
const limit = Math.min(parseInt(req.query.limit || '50'), 100);
|
||||
const limit = Math.min(parseInt(req.query.limit || '50') || 50, 100);
|
||||
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
|
|
@ -755,7 +711,7 @@ function buildRouter() {
|
|||
// ?user=maita でユーザー指定可能(将来の独立サイト対応)
|
||||
r.get('/journal/posts/public', async (req, res) => {
|
||||
try {
|
||||
const limit = Math.min(parseInt(req.query.limit || '50'), 100);
|
||||
const limit = Math.min(parseInt(req.query.limit || '50') || 50, 100);
|
||||
const userId = req.query.user || null;
|
||||
const { rows } = userId
|
||||
? await pool.query(
|
||||
|
|
@ -1006,7 +962,7 @@ ${excerpt}
|
|||
|
||||
// GET /habit/heatmap — 過去 N 日分のチェック数(ヒートマップ用)
|
||||
r.get('/habit/heatmap', authMiddleware, async (req, res) => {
|
||||
const days = Math.min(parseInt(req.query.days || '90'), 365);
|
||||
const days = Math.min(parseInt(req.query.days || '90') || 90, 365);
|
||||
try {
|
||||
const { rows } = await pool.query(`
|
||||
SELECT log_date::text AS date, COUNT(*) AS count
|
||||
|
|
@ -1039,7 +995,11 @@ ${excerpt}
|
|||
INSERT INTO pulse_log (user_id, log_date, mood, energy, focus, note, updated_at)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,NOW())
|
||||
ON CONFLICT (user_id, log_date) DO UPDATE
|
||||
SET mood=$3, energy=$4, focus=$5, note=$6, updated_at=NOW()
|
||||
SET mood=COALESCE($3, pulse_log.mood),
|
||||
energy=COALESCE($4, pulse_log.energy),
|
||||
focus=COALESCE($5, pulse_log.focus),
|
||||
note=COALESCE(NULLIF($6,''), pulse_log.note),
|
||||
updated_at=NOW()
|
||||
RETURNING mood, energy, focus, note
|
||||
`, [req.userId, req.params.date, mood || null, energy || null, focus || null, note]);
|
||||
res.json({ entry: rows[0] });
|
||||
|
|
@ -1048,7 +1008,7 @@ ${excerpt}
|
|||
|
||||
// GET /pulse/log — 範囲取得(デフォルト直近30日)
|
||||
r.get('/pulse/log', authMiddleware, async (req, res) => {
|
||||
const days = Math.min(parseInt(req.query.days || '30'), 365);
|
||||
const days = Math.min(parseInt(req.query.days || '30') || 30, 365);
|
||||
try {
|
||||
const { rows } = await pool.query(`
|
||||
SELECT log_date::text AS date, mood, energy, focus, note
|
||||
|
|
@ -1063,7 +1023,7 @@ ${excerpt}
|
|||
// ── Lens API ──────────────────────────────
|
||||
// GET /lens/history — スキャン履歴取得(直近 limit 件)
|
||||
r.get('/lens/history', authMiddleware, async (req, res) => {
|
||||
const limit = Math.min(parseInt(req.query.limit || '20'), 100);
|
||||
const limit = Math.min(parseInt(req.query.limit || '20') || 20, 100);
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
'SELECT id, filename, exif_data, thumbnail, scanned_at FROM lens_history WHERE user_id=$1 ORDER BY scanned_at DESC LIMIT $2',
|
||||
|
|
@ -1409,6 +1369,7 @@ ${excerpt}
|
|||
|
||||
// GET /together/groups/:groupId — グループ情報
|
||||
r.get('/together/groups/:groupId', async (req, res) => {
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(req.params.groupId)) return res.status(400).json({ error: 'invalid groupId' });
|
||||
try {
|
||||
const result = await pool.query('SELECT * FROM together_groups WHERE id=$1', [req.params.groupId]);
|
||||
if (result.rows.length === 0) return res.status(404).json({ error: 'グループが見つかりません' });
|
||||
|
|
|
|||
Loading…
Reference in New Issue