Fix database conflict and update schema
This commit is contained in:
		@@ -10,6 +10,8 @@ import {
 | 
				
			|||||||
  getFeedByUrl,
 | 
					  getFeedByUrl,
 | 
				
			||||||
  fetchAllEpisodes,
 | 
					  fetchAllEpisodes,
 | 
				
			||||||
  fetchEpisodesWithArticles,
 | 
					  fetchEpisodesWithArticles,
 | 
				
			||||||
 | 
					  getFeedRequests,
 | 
				
			||||||
 | 
					  updateFeedRequestStatus,
 | 
				
			||||||
} from "./services/database.js";
 | 
					} from "./services/database.js";
 | 
				
			||||||
import { batchProcess, addNewFeedUrl } from "./scripts/fetch_and_generate.js";
 | 
					import { batchProcess, addNewFeedUrl } from "./scripts/fetch_and_generate.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -200,17 +202,102 @@ app.get("/api/admin/episodes/simple", async (c) => {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Feed requests management
 | 
				
			||||||
 | 
					app.get("/api/admin/feed-requests", async (c) => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const status = c.req.query("status");
 | 
				
			||||||
 | 
					    const requests = await getFeedRequests(status);
 | 
				
			||||||
 | 
					    return c.json(requests);
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error("Error fetching feed requests:", error);
 | 
				
			||||||
 | 
					    return c.json({ error: "Failed to fetch feed requests" }, 500);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app.patch("/api/admin/feed-requests/:id/approve", async (c) => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const requestId = c.req.param("id");
 | 
				
			||||||
 | 
					    const body = await c.req.json();
 | 
				
			||||||
 | 
					    const { adminNotes } = body;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // First get the request to get the URL
 | 
				
			||||||
 | 
					    const requests = await getFeedRequests();
 | 
				
			||||||
 | 
					    const request = requests.find(r => r.id === requestId);
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if (!request) {
 | 
				
			||||||
 | 
					      return c.json({ error: "Feed request not found" }, 404);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if (request.status !== 'pending') {
 | 
				
			||||||
 | 
					      return c.json({ error: "Feed request already processed" }, 400);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Add the feed
 | 
				
			||||||
 | 
					    await addNewFeedUrl(request.url);
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Update request status
 | 
				
			||||||
 | 
					    const updated = await updateFeedRequestStatus(
 | 
				
			||||||
 | 
					      requestId, 
 | 
				
			||||||
 | 
					      'approved', 
 | 
				
			||||||
 | 
					      'admin',
 | 
				
			||||||
 | 
					      adminNotes
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if (!updated) {
 | 
				
			||||||
 | 
					      return c.json({ error: "Failed to update request status" }, 500);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return c.json({ 
 | 
				
			||||||
 | 
					      success: true, 
 | 
				
			||||||
 | 
					      message: "Feed request approved and feed added successfully" 
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error("Error approving feed request:", error);
 | 
				
			||||||
 | 
					    return c.json({ error: "Failed to approve feed request" }, 500);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app.patch("/api/admin/feed-requests/:id/reject", async (c) => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const requestId = c.req.param("id");
 | 
				
			||||||
 | 
					    const body = await c.req.json();
 | 
				
			||||||
 | 
					    const { adminNotes } = body;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    const updated = await updateFeedRequestStatus(
 | 
				
			||||||
 | 
					      requestId, 
 | 
				
			||||||
 | 
					      'rejected', 
 | 
				
			||||||
 | 
					      'admin',
 | 
				
			||||||
 | 
					      adminNotes
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if (!updated) {
 | 
				
			||||||
 | 
					      return c.json({ error: "Feed request not found" }, 404);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return c.json({ 
 | 
				
			||||||
 | 
					      success: true, 
 | 
				
			||||||
 | 
					      message: "Feed request rejected successfully" 
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error("Error rejecting feed request:", error);
 | 
				
			||||||
 | 
					    return c.json({ error: "Failed to reject feed request" }, 500);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// System management
 | 
					// System management
 | 
				
			||||||
app.get("/api/admin/stats", async (c) => {
 | 
					app.get("/api/admin/stats", async (c) => {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const feeds = await getAllFeedsIncludingInactive();
 | 
					    const feeds = await getAllFeedsIncludingInactive();
 | 
				
			||||||
    const episodes = await fetchAllEpisodes();
 | 
					    const episodes = await fetchAllEpisodes();
 | 
				
			||||||
 | 
					    const feedRequests = await getFeedRequests();
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    const stats = {
 | 
					    const stats = {
 | 
				
			||||||
      totalFeeds: feeds.length,
 | 
					      totalFeeds: feeds.length,
 | 
				
			||||||
      activeFeeds: feeds.filter(f => f.active).length,
 | 
					      activeFeeds: feeds.filter(f => f.active).length,
 | 
				
			||||||
      inactiveFeeds: feeds.filter(f => !f.active).length,
 | 
					      inactiveFeeds: feeds.filter(f => !f.active).length,
 | 
				
			||||||
      totalEpisodes: episodes.length,
 | 
					      totalEpisodes: episodes.length,
 | 
				
			||||||
 | 
					      pendingRequests: feedRequests.filter(r => r.status === 'pending').length,
 | 
				
			||||||
 | 
					      totalRequests: feedRequests.length,
 | 
				
			||||||
      lastUpdated: new Date().toISOString(),
 | 
					      lastUpdated: new Date().toISOString(),
 | 
				
			||||||
      adminPort: config.admin.port,
 | 
					      adminPort: config.admin.port,
 | 
				
			||||||
      authEnabled: !!(config.admin.username && config.admin.password),
 | 
					      authEnabled: !!(config.admin.username && config.admin.password),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,7 +23,7 @@ function App() {
 | 
				
			|||||||
          className={`tab ${activeTab === 'feeds' ? 'active' : ''}`}
 | 
					          className={`tab ${activeTab === 'feeds' ? 'active' : ''}`}
 | 
				
			||||||
          onClick={() => setActiveTab('feeds')}
 | 
					          onClick={() => setActiveTab('feeds')}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          フィード管理
 | 
					          フィードリクエスト
 | 
				
			||||||
        </button>
 | 
					        </button>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,141 +1,61 @@
 | 
				
			|||||||
import { useState, useEffect } from 'react'
 | 
					import { useState } from 'react'
 | 
				
			||||||
 | 
					 | 
				
			||||||
interface Feed {
 | 
					 | 
				
			||||||
  id: string
 | 
					 | 
				
			||||||
  url: string
 | 
					 | 
				
			||||||
  title?: string
 | 
					 | 
				
			||||||
  description?: string
 | 
					 | 
				
			||||||
  active: boolean
 | 
					 | 
				
			||||||
  lastUpdated?: string
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
function FeedManager() {
 | 
					function FeedManager() {
 | 
				
			||||||
  const [feeds, setFeeds] = useState<Feed[]>([])
 | 
					 | 
				
			||||||
  const [loading, setLoading] = useState(true)
 | 
					 | 
				
			||||||
  const [error, setError] = useState<string | null>(null)
 | 
					  const [error, setError] = useState<string | null>(null)
 | 
				
			||||||
  const [success, setSuccess] = useState<string | null>(null)
 | 
					  const [success, setSuccess] = useState<string | null>(null)
 | 
				
			||||||
  const [newFeedUrl, setNewFeedUrl] = useState('')
 | 
					  const [newFeedUrl, setNewFeedUrl] = useState('')
 | 
				
			||||||
  const [adding, setAdding] = useState(false)
 | 
					  const [requestMessage, setRequestMessage] = useState('')
 | 
				
			||||||
 | 
					  const [requesting, setRequesting] = useState(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  const submitRequest = async (e: React.FormEvent) => {
 | 
				
			||||||
    fetchFeeds()
 | 
					 | 
				
			||||||
  }, [])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const fetchFeeds = async () => {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      setLoading(true)
 | 
					 | 
				
			||||||
      const response = await fetch('/api/feeds')
 | 
					 | 
				
			||||||
      if (!response.ok) throw new Error('フィードの取得に失敗しました')
 | 
					 | 
				
			||||||
      const data = await response.json()
 | 
					 | 
				
			||||||
      setFeeds(data)
 | 
					 | 
				
			||||||
    } catch (err) {
 | 
					 | 
				
			||||||
      setError(err instanceof Error ? err.message : 'エラーが発生しました')
 | 
					 | 
				
			||||||
    } finally {
 | 
					 | 
				
			||||||
      setLoading(false)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const addFeed = async (e: React.FormEvent) => {
 | 
					 | 
				
			||||||
    e.preventDefault()
 | 
					    e.preventDefault()
 | 
				
			||||||
    if (!newFeedUrl.trim()) return
 | 
					    if (!newFeedUrl.trim()) return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      setAdding(true)
 | 
					      setRequesting(true)
 | 
				
			||||||
      setError(null)
 | 
					      setError(null)
 | 
				
			||||||
      setSuccess(null)
 | 
					      setSuccess(null)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const response = await fetch('/api/feeds', {
 | 
					      const response = await fetch('/api/feed-requests', {
 | 
				
			||||||
        method: 'POST',
 | 
					        method: 'POST',
 | 
				
			||||||
        headers: {
 | 
					        headers: {
 | 
				
			||||||
          'Content-Type': 'application/json',
 | 
					          'Content-Type': 'application/json',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        body: JSON.stringify({ url: newFeedUrl.trim() }),
 | 
					        body: JSON.stringify({ 
 | 
				
			||||||
 | 
					          url: newFeedUrl.trim(),
 | 
				
			||||||
 | 
					          requestMessage: requestMessage.trim() || undefined
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (!response.ok) {
 | 
					      if (!response.ok) {
 | 
				
			||||||
        const errorData = await response.json()
 | 
					        const errorData = await response.json()
 | 
				
			||||||
        throw new Error(errorData.error || 'フィードの追加に失敗しました')
 | 
					        throw new Error(errorData.error || 'リクエストの送信に失敗しました')
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      setSuccess('フィードを追加しました')
 | 
					      setSuccess('フィードリクエストを送信しました。管理者の承認をお待ちください。')
 | 
				
			||||||
      setNewFeedUrl('')
 | 
					      setNewFeedUrl('')
 | 
				
			||||||
      await fetchFeeds()
 | 
					      setRequestMessage('')
 | 
				
			||||||
    } catch (err) {
 | 
					    } catch (err) {
 | 
				
			||||||
      setError(err instanceof Error ? err.message : 'エラーが発生しました')
 | 
					      setError(err instanceof Error ? err.message : 'エラーが発生しました')
 | 
				
			||||||
    } finally {
 | 
					    } finally {
 | 
				
			||||||
      setAdding(false)
 | 
					      setRequesting(false)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const deleteFeed = async (feedId: string) => {
 | 
					 | 
				
			||||||
    if (!confirm('このフィードを削除しますか?関連するエピソードも削除されます。')) {
 | 
					 | 
				
			||||||
      return
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      setError(null)
 | 
					 | 
				
			||||||
      setSuccess(null)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const response = await fetch(`/api/feeds/${feedId}`, {
 | 
					 | 
				
			||||||
        method: 'DELETE',
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (!response.ok) {
 | 
					 | 
				
			||||||
        const errorData = await response.json()
 | 
					 | 
				
			||||||
        throw new Error(errorData.error || 'フィードの削除に失敗しました')
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      setSuccess('フィードを削除しました')
 | 
					 | 
				
			||||||
      await fetchFeeds()
 | 
					 | 
				
			||||||
    } catch (err) {
 | 
					 | 
				
			||||||
      setError(err instanceof Error ? err.message : 'エラーが発生しました')
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const toggleFeed = async (feedId: string, active: boolean) => {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      setError(null)
 | 
					 | 
				
			||||||
      setSuccess(null)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const response = await fetch(`/api/feeds/${feedId}/toggle`, {
 | 
					 | 
				
			||||||
        method: 'PATCH',
 | 
					 | 
				
			||||||
        headers: {
 | 
					 | 
				
			||||||
          'Content-Type': 'application/json',
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        body: JSON.stringify({ active }),
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (!response.ok) {
 | 
					 | 
				
			||||||
        const errorData = await response.json()
 | 
					 | 
				
			||||||
        throw new Error(errorData.error || 'フィードの状態変更に失敗しました')
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      setSuccess(`フィードを${active ? '有効' : '無効'}にしました`)
 | 
					 | 
				
			||||||
      await fetchFeeds()
 | 
					 | 
				
			||||||
    } catch (err) {
 | 
					 | 
				
			||||||
      setError(err instanceof Error ? err.message : 'エラーが発生しました')
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const formatDate = (dateString?: string) => {
 | 
					 | 
				
			||||||
    if (!dateString) return '未更新'
 | 
					 | 
				
			||||||
    return new Date(dateString).toLocaleString('ja-JP')
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (loading) {
 | 
					 | 
				
			||||||
    return <div className="loading">読み込み中...</div>
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
      {error && <div className="error">{error}</div>}
 | 
					      {error && <div className="error">{error}</div>}
 | 
				
			||||||
      {success && <div className="success">{success}</div>}
 | 
					      {success && <div className="success">{success}</div>}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div style={{ marginBottom: '30px' }}>
 | 
					      <div style={{ marginBottom: '30px' }}>
 | 
				
			||||||
        <h2>新しいフィードを追加</h2>
 | 
					        <h2>新しいフィードをリクエスト</h2>
 | 
				
			||||||
        <form onSubmit={addFeed}>
 | 
					        <p style={{ color: '#666', marginBottom: '20px' }}>
 | 
				
			||||||
 | 
					          追加したいRSSフィードのURLを送信してください。管理者が承認後、フィードが追加されます。
 | 
				
			||||||
 | 
					        </p>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <form onSubmit={submitRequest}>
 | 
				
			||||||
          <div className="form-group">
 | 
					          <div className="form-group">
 | 
				
			||||||
            <label className="form-label">RSS フィード URL</label>
 | 
					            <label className="form-label">RSS フィード URL *</label>
 | 
				
			||||||
            <input
 | 
					            <input
 | 
				
			||||||
              type="url"
 | 
					              type="url"
 | 
				
			||||||
              className="form-input"
 | 
					              className="form-input"
 | 
				
			||||||
@@ -145,66 +65,37 @@ function FeedManager() {
 | 
				
			|||||||
              required
 | 
					              required
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          <div className="form-group">
 | 
				
			||||||
 | 
					            <label className="form-label">メッセージ(任意)</label>
 | 
				
			||||||
 | 
					            <textarea
 | 
				
			||||||
 | 
					              className="form-input"
 | 
				
			||||||
 | 
					              value={requestMessage}
 | 
				
			||||||
 | 
					              onChange={(e) => setRequestMessage(e.target.value)}
 | 
				
			||||||
 | 
					              placeholder="このフィードについての説明や追加理由があれば記載してください"
 | 
				
			||||||
 | 
					              rows={3}
 | 
				
			||||||
 | 
					              style={{ resize: 'vertical', minHeight: '80px' }}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
          <button 
 | 
					          <button 
 | 
				
			||||||
            type="submit" 
 | 
					            type="submit" 
 | 
				
			||||||
            className="btn btn-primary"
 | 
					            className="btn btn-primary"
 | 
				
			||||||
            disabled={adding}
 | 
					            disabled={requesting}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            {adding ? '追加中...' : 'フィードを追加'}
 | 
					            {requesting ? 'リクエスト送信中...' : 'フィードをリクエスト'}
 | 
				
			||||||
          </button>
 | 
					          </button>
 | 
				
			||||||
        </form>
 | 
					        </form>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div>
 | 
					      <div style={{ backgroundColor: '#f8f9fa', padding: '20px', borderRadius: '8px' }}>
 | 
				
			||||||
        <h2>登録済みフィード ({feeds.length}件)</h2>
 | 
					        <h3 style={{ marginBottom: '15px' }}>フィードリクエストについて</h3>
 | 
				
			||||||
        
 | 
					        <ul style={{ paddingLeft: '20px', color: '#666' }}>
 | 
				
			||||||
        {feeds.length === 0 ? (
 | 
					          <li>送信されたフィードリクエストは管理者が確認します</li>
 | 
				
			||||||
          <div className="empty-state">
 | 
					          <li>適切なRSSフィードと判断された場合、承認されて自動的に追加されます</li>
 | 
				
			||||||
            <p>登録されているフィードがありません</p>
 | 
					          <li>承認までにお時間をいただく場合があります</li>
 | 
				
			||||||
          </div>
 | 
					          <li>不適切なフィードや重複フィードは拒否される場合があります</li>
 | 
				
			||||||
        ) : (
 | 
					        </ul>
 | 
				
			||||||
          <div>
 | 
					 | 
				
			||||||
            {feeds.map((feed) => (
 | 
					 | 
				
			||||||
              <div key={feed.id} className="feed-item">
 | 
					 | 
				
			||||||
                <div className="feed-info">
 | 
					 | 
				
			||||||
                  <div className="feed-title">
 | 
					 | 
				
			||||||
                    {feed.title || 'タイトル不明'}
 | 
					 | 
				
			||||||
                    {!feed.active && (
 | 
					 | 
				
			||||||
                      <span style={{ 
 | 
					 | 
				
			||||||
                        marginLeft: '8px', 
 | 
					 | 
				
			||||||
                        padding: '2px 6px', 
 | 
					 | 
				
			||||||
                        backgroundColor: '#dc3545', 
 | 
					 | 
				
			||||||
                        color: 'white', 
 | 
					 | 
				
			||||||
                        fontSize: '12px', 
 | 
					 | 
				
			||||||
                        borderRadius: '3px' 
 | 
					 | 
				
			||||||
                      }}>
 | 
					 | 
				
			||||||
                        無効
 | 
					 | 
				
			||||||
                      </span>
 | 
					 | 
				
			||||||
                    )}
 | 
					 | 
				
			||||||
                  </div>
 | 
					 | 
				
			||||||
                  <div className="feed-url">{feed.url}</div>
 | 
					 | 
				
			||||||
                  <div style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
 | 
					 | 
				
			||||||
                    最終更新: {formatDate(feed.lastUpdated)}
 | 
					 | 
				
			||||||
                  </div>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
                <div className="feed-actions">
 | 
					 | 
				
			||||||
                  <button
 | 
					 | 
				
			||||||
                    className={`btn ${feed.active ? 'btn-secondary' : 'btn-primary'}`}
 | 
					 | 
				
			||||||
                    onClick={() => toggleFeed(feed.id, !feed.active)}
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    {feed.active ? '無効化' : '有効化'}
 | 
					 | 
				
			||||||
                  </button>
 | 
					 | 
				
			||||||
                  <button
 | 
					 | 
				
			||||||
                    className="btn btn-danger"
 | 
					 | 
				
			||||||
                    onClick={() => deleteFeed(feed.id)}
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    削除
 | 
					 | 
				
			||||||
                  </button>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
              </div>
 | 
					 | 
				
			||||||
            ))}
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        )}
 | 
					 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										15
									
								
								schema.sql
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								schema.sql
									
									
									
									
									
								
							@@ -53,6 +53,19 @@ CREATE TABLE IF NOT EXISTS tts_queue (
 | 
				
			|||||||
  status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'failed'))
 | 
					  status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'failed'))
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- Feed requests from users
 | 
				
			||||||
 | 
					CREATE TABLE IF NOT EXISTS feed_requests (
 | 
				
			||||||
 | 
					  id TEXT PRIMARY KEY,
 | 
				
			||||||
 | 
					  url TEXT NOT NULL,
 | 
				
			||||||
 | 
					  requested_by TEXT,
 | 
				
			||||||
 | 
					  request_message TEXT,
 | 
				
			||||||
 | 
					  status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected')),
 | 
				
			||||||
 | 
					  created_at TEXT NOT NULL,
 | 
				
			||||||
 | 
					  reviewed_at TEXT,
 | 
				
			||||||
 | 
					  reviewed_by TEXT,
 | 
				
			||||||
 | 
					  admin_notes TEXT
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-- Create indexes for better performance
 | 
					-- Create indexes for better performance
 | 
				
			||||||
CREATE INDEX IF NOT EXISTS idx_articles_feed_id ON articles(feed_id);
 | 
					CREATE INDEX IF NOT EXISTS idx_articles_feed_id ON articles(feed_id);
 | 
				
			||||||
CREATE INDEX IF NOT EXISTS idx_articles_pub_date ON articles(pub_date);
 | 
					CREATE INDEX IF NOT EXISTS idx_articles_pub_date ON articles(pub_date);
 | 
				
			||||||
@@ -61,3 +74,5 @@ CREATE INDEX IF NOT EXISTS idx_episodes_article_id ON episodes(article_id);
 | 
				
			|||||||
CREATE INDEX IF NOT EXISTS idx_feeds_active ON feeds(active);
 | 
					CREATE INDEX IF NOT EXISTS idx_feeds_active ON feeds(active);
 | 
				
			||||||
CREATE INDEX IF NOT EXISTS idx_tts_queue_status ON tts_queue(status);
 | 
					CREATE INDEX IF NOT EXISTS idx_tts_queue_status ON tts_queue(status);
 | 
				
			||||||
CREATE INDEX IF NOT EXISTS idx_tts_queue_created_at ON tts_queue(created_at);
 | 
					CREATE INDEX IF NOT EXISTS idx_tts_queue_created_at ON tts_queue(created_at);
 | 
				
			||||||
 | 
					CREATE INDEX IF NOT EXISTS idx_feed_requests_status ON feed_requests(status);
 | 
				
			||||||
 | 
					CREATE INDEX IF NOT EXISTS idx_feed_requests_created_at ON feed_requests(created_at);
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										74
									
								
								server.ts
									
									
									
									
									
								
							
							
						
						
									
										74
									
								
								server.ts
									
									
									
									
									
								
							@@ -119,73 +119,29 @@ app.get("/api/episodes", async (c) => {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
app.get("/api/feeds", async (c) => {
 | 
					app.post("/api/feed-requests", async (c) => {
 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const { getAllFeedsIncludingInactive } = await import("./services/database.js");
 | 
					 | 
				
			||||||
    const feeds = await getAllFeedsIncludingInactive();
 | 
					 | 
				
			||||||
    return c.json(feeds);
 | 
					 | 
				
			||||||
  } catch (error) {
 | 
					 | 
				
			||||||
    console.error("Error fetching feeds:", error);
 | 
					 | 
				
			||||||
    return c.json({ error: "Failed to fetch feeds" }, 500);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
app.post("/api/feeds", async (c) => {
 | 
					 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const body = await c.req.json();
 | 
					    const body = await c.req.json();
 | 
				
			||||||
    const { url } = body;
 | 
					    const { url, requestMessage } = body;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    if (!url || typeof url !== 'string') {
 | 
					    if (!url || typeof url !== 'string') {
 | 
				
			||||||
      return c.json({ error: "URL is required" }, 400);
 | 
					      return c.json({ error: "URL is required" }, 400);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { addNewFeedUrl } = await import("./scripts/fetch_and_generate.js");
 | 
					    const { submitFeedRequest } = await import("./services/database.js");
 | 
				
			||||||
    await addNewFeedUrl(url);
 | 
					    const requestId = await submitFeedRequest({
 | 
				
			||||||
    return c.json({ success: true, message: "Feed added successfully" });
 | 
					      url,
 | 
				
			||||||
 | 
					      requestMessage,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return c.json({ 
 | 
				
			||||||
 | 
					      success: true, 
 | 
				
			||||||
 | 
					      message: "Feed request submitted successfully",
 | 
				
			||||||
 | 
					      requestId 
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
    console.error("Error adding feed:", error);
 | 
					    console.error("Error submitting feed request:", error);
 | 
				
			||||||
    return c.json({ error: "Failed to add feed" }, 500);
 | 
					    return c.json({ error: "Failed to submit feed request" }, 500);
 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
app.delete("/api/feeds/:id", async (c) => {
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const feedId = c.req.param("id");
 | 
					 | 
				
			||||||
    const { deleteFeed } = await import("./services/database.js");
 | 
					 | 
				
			||||||
    const success = await deleteFeed(feedId);
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    if (!success) {
 | 
					 | 
				
			||||||
      return c.json({ error: "Feed not found" }, 404);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    return c.json({ success: true, message: "Feed deleted successfully" });
 | 
					 | 
				
			||||||
  } catch (error) {
 | 
					 | 
				
			||||||
    console.error("Error deleting feed:", error);
 | 
					 | 
				
			||||||
    return c.json({ error: "Failed to delete feed" }, 500);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
app.patch("/api/feeds/:id/toggle", async (c) => {
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const feedId = c.req.param("id");
 | 
					 | 
				
			||||||
    const body = await c.req.json();
 | 
					 | 
				
			||||||
    const { active } = body;
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    if (typeof active !== 'boolean') {
 | 
					 | 
				
			||||||
      return c.json({ error: "Active status must be boolean" }, 400);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const { toggleFeedActive } = await import("./services/database.js");
 | 
					 | 
				
			||||||
    const success = await toggleFeedActive(feedId, active);
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    if (!success) {
 | 
					 | 
				
			||||||
      return c.json({ error: "Feed not found" }, 404);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    return c.json({ success: true, message: "Feed status updated successfully" });
 | 
					 | 
				
			||||||
  } catch (error) {
 | 
					 | 
				
			||||||
    console.error("Error toggling feed:", error);
 | 
					 | 
				
			||||||
    return c.json({ error: "Failed to update feed status" }, 500);
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,6 +16,12 @@ function initializeDatabase(): Database {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const db = new Database(config.paths.dbPath);
 | 
					  const db = new Database(config.paths.dbPath);
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // Enable WAL mode for better concurrent access
 | 
				
			||||||
 | 
					  db.exec("PRAGMA journal_mode = WAL;");
 | 
				
			||||||
 | 
					  db.exec("PRAGMA synchronous = NORMAL;");
 | 
				
			||||||
 | 
					  db.exec("PRAGMA cache_size = 1000;");
 | 
				
			||||||
 | 
					  db.exec("PRAGMA temp_store = memory;");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Ensure schema is set up - use the complete schema
 | 
					  // Ensure schema is set up - use the complete schema
 | 
				
			||||||
  db.exec(`CREATE TABLE IF NOT EXISTS feeds (
 | 
					  db.exec(`CREATE TABLE IF NOT EXISTS feeds (
 | 
				
			||||||
@@ -70,13 +76,27 @@ function initializeDatabase(): Database {
 | 
				
			|||||||
    status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'failed'))
 | 
					    status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'failed'))
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  CREATE TABLE IF NOT EXISTS feed_requests (
 | 
				
			||||||
 | 
					    id TEXT PRIMARY KEY,
 | 
				
			||||||
 | 
					    url TEXT NOT NULL,
 | 
				
			||||||
 | 
					    requested_by TEXT,
 | 
				
			||||||
 | 
					    request_message TEXT,
 | 
				
			||||||
 | 
					    status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected')),
 | 
				
			||||||
 | 
					    created_at TEXT NOT NULL,
 | 
				
			||||||
 | 
					    reviewed_at TEXT,
 | 
				
			||||||
 | 
					    reviewed_by TEXT,
 | 
				
			||||||
 | 
					    admin_notes TEXT
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  CREATE INDEX IF NOT EXISTS idx_articles_feed_id ON articles(feed_id);
 | 
					  CREATE INDEX IF NOT EXISTS idx_articles_feed_id ON articles(feed_id);
 | 
				
			||||||
  CREATE INDEX IF NOT EXISTS idx_articles_pub_date ON articles(pub_date);
 | 
					  CREATE INDEX IF NOT EXISTS idx_articles_pub_date ON articles(pub_date);
 | 
				
			||||||
  CREATE INDEX IF NOT EXISTS idx_articles_processed ON articles(processed);
 | 
					  CREATE INDEX IF NOT EXISTS idx_articles_processed ON articles(processed);
 | 
				
			||||||
  CREATE INDEX IF NOT EXISTS idx_episodes_article_id ON episodes(article_id);
 | 
					  CREATE INDEX IF NOT EXISTS idx_episodes_article_id ON episodes(article_id);
 | 
				
			||||||
  CREATE INDEX IF NOT EXISTS idx_feeds_active ON feeds(active);
 | 
					  CREATE INDEX IF NOT EXISTS idx_feeds_active ON feeds(active);
 | 
				
			||||||
  CREATE INDEX IF NOT EXISTS idx_tts_queue_status ON tts_queue(status);
 | 
					  CREATE INDEX IF NOT EXISTS idx_tts_queue_status ON tts_queue(status);
 | 
				
			||||||
  CREATE INDEX IF NOT EXISTS idx_tts_queue_created_at ON tts_queue(created_at);`);
 | 
					  CREATE INDEX IF NOT EXISTS idx_tts_queue_created_at ON tts_queue(created_at);
 | 
				
			||||||
 | 
					  CREATE INDEX IF NOT EXISTS idx_feed_requests_status ON feed_requests(status);
 | 
				
			||||||
 | 
					  CREATE INDEX IF NOT EXISTS idx_feed_requests_created_at ON feed_requests(created_at);`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return db;
 | 
					  return db;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -601,6 +621,83 @@ export async function removeFromQueue(queueId: string): Promise<void> {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Feed Request management functions
 | 
				
			||||||
 | 
					export interface FeedRequest {
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  url: string;
 | 
				
			||||||
 | 
					  requestedBy?: string;
 | 
				
			||||||
 | 
					  requestMessage?: string;
 | 
				
			||||||
 | 
					  status: 'pending' | 'approved' | 'rejected';
 | 
				
			||||||
 | 
					  createdAt: string;
 | 
				
			||||||
 | 
					  reviewedAt?: string;
 | 
				
			||||||
 | 
					  reviewedBy?: string;
 | 
				
			||||||
 | 
					  adminNotes?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function submitFeedRequest(
 | 
				
			||||||
 | 
					  request: Omit<FeedRequest, "id" | "createdAt" | "status">
 | 
				
			||||||
 | 
					): Promise<string> {
 | 
				
			||||||
 | 
					  const id = crypto.randomUUID();
 | 
				
			||||||
 | 
					  const createdAt = new Date().toISOString();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const stmt = db.prepare(
 | 
				
			||||||
 | 
					      "INSERT INTO feed_requests (id, url, requested_by, request_message, status, created_at) VALUES (?, ?, ?, ?, 'pending', ?)",
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    stmt.run(id, request.url, request.requestedBy || null, request.requestMessage || null, createdAt);
 | 
				
			||||||
 | 
					    console.log(`Feed request submitted: ${request.url}`);
 | 
				
			||||||
 | 
					    return id;
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error("Error submitting feed request:", error);
 | 
				
			||||||
 | 
					    throw error;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function getFeedRequests(status?: string): Promise<FeedRequest[]> {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const sql = status
 | 
				
			||||||
 | 
					      ? "SELECT * FROM feed_requests WHERE status = ? ORDER BY created_at DESC"
 | 
				
			||||||
 | 
					      : "SELECT * FROM feed_requests ORDER BY created_at DESC";
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    const stmt = db.prepare(sql);
 | 
				
			||||||
 | 
					    const rows = status ? stmt.all(status) : stmt.all();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (rows as any[]).map((row) => ({
 | 
				
			||||||
 | 
					      id: row.id,
 | 
				
			||||||
 | 
					      url: row.url,
 | 
				
			||||||
 | 
					      requestedBy: row.requested_by,
 | 
				
			||||||
 | 
					      requestMessage: row.request_message,
 | 
				
			||||||
 | 
					      status: row.status,
 | 
				
			||||||
 | 
					      createdAt: row.created_at,
 | 
				
			||||||
 | 
					      reviewedAt: row.reviewed_at,
 | 
				
			||||||
 | 
					      reviewedBy: row.reviewed_by,
 | 
				
			||||||
 | 
					      adminNotes: row.admin_notes,
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error("Error getting feed requests:", error);
 | 
				
			||||||
 | 
					    throw error;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function updateFeedRequestStatus(
 | 
				
			||||||
 | 
					  requestId: string,
 | 
				
			||||||
 | 
					  status: 'approved' | 'rejected',
 | 
				
			||||||
 | 
					  reviewedBy?: string,
 | 
				
			||||||
 | 
					  adminNotes?: string,
 | 
				
			||||||
 | 
					): Promise<boolean> {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const reviewedAt = new Date().toISOString();
 | 
				
			||||||
 | 
					    const stmt = db.prepare(
 | 
				
			||||||
 | 
					      "UPDATE feed_requests SET status = ?, reviewed_at = ?, reviewed_by = ?, admin_notes = ? WHERE id = ?",
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const result = stmt.run(status, reviewedAt, reviewedBy || null, adminNotes || null, requestId);
 | 
				
			||||||
 | 
					    return result.changes > 0;
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error("Error updating feed request status:", error);
 | 
				
			||||||
 | 
					    throw error;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function closeDatabase(): void {
 | 
					export function closeDatabase(): void {
 | 
				
			||||||
  db.close();
 | 
					  db.close();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user