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 title = og('og:title') || doc.querySelector('title')?.text || url;
|
||||||
const desc = og('og:description') || meta('description') || '';
|
const desc = og('og:description') || meta('description') || '';
|
||||||
const img = og('og:image') || '';
|
const img = og('og:image') || '';
|
||||||
let host; // Declare host here for broader scope
|
|
||||||
|
|
||||||
// Google Favicon API(優先)→ favicon.ico(フォールバック)
|
// Google Favicon API(優先)→ favicon.ico(フォールバック)
|
||||||
const faviconUrl = `https://www.google.com/s2/favicons?domain=${new URL(url).hostname}&sz=32`;
|
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());
|
console.error('[Gemini] Raw response:', result.response.text());
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
summary: `⚠️ AI分析失敗: ${String(e)}`,
|
summary: 'AI分析に失敗しました。しばらく後にお試しください。',
|
||||||
topics: ['その他'],
|
topics: ['その他'],
|
||||||
readingTime: 3
|
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 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 DROP CONSTRAINT IF EXISTS site_config_pkey`,
|
||||||
`ALTER TABLE site_config ADD PRIMARY KEY (user_id, key)`,
|
`ALTER TABLE site_config ADD PRIMARY KEY (user_id, key)`,
|
||||||
// together テーブルをクリーンリセット(古いスキーマを完全に作り直す)
|
// together スキーマは schema 配列の CREATE TABLE IF NOT EXISTS で管理
|
||||||
`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)`,
|
|
||||||
];
|
];
|
||||||
for (const sql of migrations) {
|
for (const sql of migrations) {
|
||||||
await pool.query(sql).catch(e => console.warn('[DB] Migration warning:', e.message));
|
await pool.query(sql).catch(e => console.warn('[DB] Migration warning:', e.message));
|
||||||
|
|
@ -728,7 +684,7 @@ function buildRouter() {
|
||||||
|
|
||||||
// GET /api/history - 履歴取得
|
// GET /api/history - 履歴取得
|
||||||
r.get('/history', authMiddleware, async (req, res) => {
|
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 {
|
try {
|
||||||
const result = await pool.query(`
|
const result = await pool.query(`
|
||||||
|
|
@ -755,7 +711,7 @@ function buildRouter() {
|
||||||
// ?user=maita でユーザー指定可能(将来の独立サイト対応)
|
// ?user=maita でユーザー指定可能(将来の独立サイト対応)
|
||||||
r.get('/journal/posts/public', async (req, res) => {
|
r.get('/journal/posts/public', async (req, res) => {
|
||||||
try {
|
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 userId = req.query.user || null;
|
||||||
const { rows } = userId
|
const { rows } = userId
|
||||||
? await pool.query(
|
? await pool.query(
|
||||||
|
|
@ -1006,7 +962,7 @@ ${excerpt}
|
||||||
|
|
||||||
// GET /habit/heatmap — 過去 N 日分のチェック数(ヒートマップ用)
|
// GET /habit/heatmap — 過去 N 日分のチェック数(ヒートマップ用)
|
||||||
r.get('/habit/heatmap', authMiddleware, async (req, res) => {
|
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 {
|
try {
|
||||||
const { rows } = await pool.query(`
|
const { rows } = await pool.query(`
|
||||||
SELECT log_date::text AS date, COUNT(*) AS count
|
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)
|
INSERT INTO pulse_log (user_id, log_date, mood, energy, focus, note, updated_at)
|
||||||
VALUES ($1,$2,$3,$4,$5,$6,NOW())
|
VALUES ($1,$2,$3,$4,$5,$6,NOW())
|
||||||
ON CONFLICT (user_id, log_date) DO UPDATE
|
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
|
RETURNING mood, energy, focus, note
|
||||||
`, [req.userId, req.params.date, mood || null, energy || null, focus || null, note]);
|
`, [req.userId, req.params.date, mood || null, energy || null, focus || null, note]);
|
||||||
res.json({ entry: rows[0] });
|
res.json({ entry: rows[0] });
|
||||||
|
|
@ -1048,7 +1008,7 @@ ${excerpt}
|
||||||
|
|
||||||
// GET /pulse/log — 範囲取得(デフォルト直近30日)
|
// GET /pulse/log — 範囲取得(デフォルト直近30日)
|
||||||
r.get('/pulse/log', authMiddleware, async (req, res) => {
|
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 {
|
try {
|
||||||
const { rows } = await pool.query(`
|
const { rows } = await pool.query(`
|
||||||
SELECT log_date::text AS date, mood, energy, focus, note
|
SELECT log_date::text AS date, mood, energy, focus, note
|
||||||
|
|
@ -1063,7 +1023,7 @@ ${excerpt}
|
||||||
// ── Lens API ──────────────────────────────
|
// ── Lens API ──────────────────────────────
|
||||||
// GET /lens/history — スキャン履歴取得(直近 limit 件)
|
// GET /lens/history — スキャン履歴取得(直近 limit 件)
|
||||||
r.get('/lens/history', authMiddleware, async (req, res) => {
|
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 {
|
try {
|
||||||
const { rows } = await pool.query(
|
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',
|
'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 — グループ情報
|
// GET /together/groups/:groupId — グループ情報
|
||||||
r.get('/together/groups/:groupId', async (req, res) => {
|
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 {
|
try {
|
||||||
const result = await pool.query('SELECT * FROM together_groups WHERE id=$1', [req.params.groupId]);
|
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: 'グループが見つかりません' });
|
if (result.rows.length === 0) return res.status(404).json({ error: 'グループが見つかりません' });
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue