Update admin panel and server configuration
This commit is contained in:
		@@ -64,10 +64,21 @@ interface Episode {
 | 
				
			|||||||
  feedCategory?: string;
 | 
					  feedCategory?: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Setting {
 | 
				
			||||||
 | 
					  key: string;
 | 
				
			||||||
 | 
					  value: string | null;
 | 
				
			||||||
 | 
					  isCredential: boolean;
 | 
				
			||||||
 | 
					  description: string;
 | 
				
			||||||
 | 
					  defaultValue: string;
 | 
				
			||||||
 | 
					  required: boolean;
 | 
				
			||||||
 | 
					  updatedAt: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function App() {
 | 
					function App() {
 | 
				
			||||||
  const [feeds, setFeeds] = useState<Feed[]>([]);
 | 
					  const [feeds, setFeeds] = useState<Feed[]>([]);
 | 
				
			||||||
  const [stats, setStats] = useState<Stats | null>(null);
 | 
					  const [stats, setStats] = useState<Stats | null>(null);
 | 
				
			||||||
  const [envVars, setEnvVars] = useState<EnvVars>({});
 | 
					  const [envVars, setEnvVars] = useState<EnvVars>({});
 | 
				
			||||||
 | 
					  const [settings, setSettings] = useState<Setting[]>([]);
 | 
				
			||||||
  const [feedRequests, setFeedRequests] = useState<FeedRequest[]>([]);
 | 
					  const [feedRequests, setFeedRequests] = useState<FeedRequest[]>([]);
 | 
				
			||||||
  const [episodes, setEpisodes] = useState<Episode[]>([]);
 | 
					  const [episodes, setEpisodes] = useState<Episode[]>([]);
 | 
				
			||||||
  const [loading, setLoading] = useState(true);
 | 
					  const [loading, setLoading] = useState(true);
 | 
				
			||||||
@@ -80,8 +91,9 @@ function App() {
 | 
				
			|||||||
  const [approvalNotes, setApprovalNotes] = useState<{ [key: string]: string }>(
 | 
					  const [approvalNotes, setApprovalNotes] = useState<{ [key: string]: string }>(
 | 
				
			||||||
    {},
 | 
					    {},
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					  const [editingSettings, setEditingSettings] = useState<{ [key: string]: string }>({});
 | 
				
			||||||
  const [activeTab, setActiveTab] = useState<
 | 
					  const [activeTab, setActiveTab] = useState<
 | 
				
			||||||
    "dashboard" | "feeds" | "episodes" | "env" | "batch" | "requests"
 | 
					    "dashboard" | "feeds" | "episodes" | "env" | "settings" | "batch" | "requests"
 | 
				
			||||||
  >("dashboard");
 | 
					  >("dashboard");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
@@ -91,11 +103,12 @@ function App() {
 | 
				
			|||||||
  const loadData = async () => {
 | 
					  const loadData = async () => {
 | 
				
			||||||
    setLoading(true);
 | 
					    setLoading(true);
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const [feedsRes, statsRes, envRes, requestsRes, episodesRes] =
 | 
					      const [feedsRes, statsRes, envRes, settingsRes, requestsRes, episodesRes] =
 | 
				
			||||||
        await Promise.all([
 | 
					        await Promise.all([
 | 
				
			||||||
          fetch("/api/admin/feeds"),
 | 
					          fetch("/api/admin/feeds"),
 | 
				
			||||||
          fetch("/api/admin/stats"),
 | 
					          fetch("/api/admin/stats"),
 | 
				
			||||||
          fetch("/api/admin/env"),
 | 
					          fetch("/api/admin/env"),
 | 
				
			||||||
 | 
					          fetch("/api/admin/settings"),
 | 
				
			||||||
          fetch("/api/admin/feed-requests"),
 | 
					          fetch("/api/admin/feed-requests"),
 | 
				
			||||||
          fetch("/api/admin/episodes"),
 | 
					          fetch("/api/admin/episodes"),
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
@@ -104,17 +117,19 @@ function App() {
 | 
				
			|||||||
        !feedsRes.ok ||
 | 
					        !feedsRes.ok ||
 | 
				
			||||||
        !statsRes.ok ||
 | 
					        !statsRes.ok ||
 | 
				
			||||||
        !envRes.ok ||
 | 
					        !envRes.ok ||
 | 
				
			||||||
 | 
					        !settingsRes.ok ||
 | 
				
			||||||
        !requestsRes.ok ||
 | 
					        !requestsRes.ok ||
 | 
				
			||||||
        !episodesRes.ok
 | 
					        !episodesRes.ok
 | 
				
			||||||
      ) {
 | 
					      ) {
 | 
				
			||||||
        throw new Error("Failed to load data");
 | 
					        throw new Error("Failed to load data");
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const [feedsData, statsData, envData, requestsData, episodesData] =
 | 
					      const [feedsData, statsData, envData, settingsData, requestsData, episodesData] =
 | 
				
			||||||
        await Promise.all([
 | 
					        await Promise.all([
 | 
				
			||||||
          feedsRes.json(),
 | 
					          feedsRes.json(),
 | 
				
			||||||
          statsRes.json(),
 | 
					          statsRes.json(),
 | 
				
			||||||
          envRes.json(),
 | 
					          envRes.json(),
 | 
				
			||||||
 | 
					          settingsRes.json(),
 | 
				
			||||||
          requestsRes.json(),
 | 
					          requestsRes.json(),
 | 
				
			||||||
          episodesRes.json(),
 | 
					          episodesRes.json(),
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
@@ -122,6 +137,7 @@ function App() {
 | 
				
			|||||||
      setFeeds(feedsData);
 | 
					      setFeeds(feedsData);
 | 
				
			||||||
      setStats(statsData);
 | 
					      setStats(statsData);
 | 
				
			||||||
      setEnvVars(envData);
 | 
					      setEnvVars(envData);
 | 
				
			||||||
 | 
					      setSettings(settingsData);
 | 
				
			||||||
      setFeedRequests(requestsData);
 | 
					      setFeedRequests(requestsData);
 | 
				
			||||||
      setEpisodes(episodesData);
 | 
					      setEpisodes(episodesData);
 | 
				
			||||||
      setError(null);
 | 
					      setError(null);
 | 
				
			||||||
@@ -338,6 +354,43 @@ function App() {
 | 
				
			|||||||
    setApprovalNotes({ ...approvalNotes, [requestId]: notes });
 | 
					    setApprovalNotes({ ...approvalNotes, [requestId]: notes });
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const updateSetting = async (key: string, value: string) => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const res = await fetch(`/api/admin/settings/${key}`, {
 | 
				
			||||||
 | 
					        method: "PUT",
 | 
				
			||||||
 | 
					        headers: { "Content-Type": "application/json" },
 | 
				
			||||||
 | 
					        body: JSON.stringify({ value }),
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const data = await res.json();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (res.ok) {
 | 
				
			||||||
 | 
					        setSuccess(data.message);
 | 
				
			||||||
 | 
					        setEditingSettings({ ...editingSettings, [key]: "" });
 | 
				
			||||||
 | 
					        loadData();
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        setError(data.error || "設定更新に失敗しました");
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      setError("設定更新に失敗しました");
 | 
				
			||||||
 | 
					      console.error("Error updating setting:", err);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const startEditingSetting = (key: string, currentValue: string | null) => {
 | 
				
			||||||
 | 
					    setEditingSettings({ ...editingSettings, [key]: currentValue || "" });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const cancelEditingSetting = (key: string) => {
 | 
				
			||||||
 | 
					    const newEditing = { ...editingSettings };
 | 
				
			||||||
 | 
					    delete newEditing[key];
 | 
				
			||||||
 | 
					    setEditingSettings(newEditing);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const updateEditingValue = (key: string, value: string) => {
 | 
				
			||||||
 | 
					    setEditingSettings({ ...editingSettings, [key]: value });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const deleteEpisode = async (episodeId: string, episodeTitle: string) => {
 | 
					  const deleteEpisode = async (episodeId: string, episodeTitle: string) => {
 | 
				
			||||||
    if (
 | 
					    if (
 | 
				
			||||||
      !confirm(
 | 
					      !confirm(
 | 
				
			||||||
@@ -424,6 +477,12 @@ function App() {
 | 
				
			|||||||
            >
 | 
					            >
 | 
				
			||||||
              フィード承認
 | 
					              フィード承認
 | 
				
			||||||
            </button>
 | 
					            </button>
 | 
				
			||||||
 | 
					            <button
 | 
				
			||||||
 | 
					              className={`btn ${activeTab === "settings" ? "btn-primary" : "btn-secondary"}`}
 | 
				
			||||||
 | 
					              onClick={() => setActiveTab("settings")}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              設定管理
 | 
				
			||||||
 | 
					            </button>
 | 
				
			||||||
            <button
 | 
					            <button
 | 
				
			||||||
              className={`btn ${activeTab === "env" ? "btn-primary" : "btn-secondary"}`}
 | 
					              className={`btn ${activeTab === "env" ? "btn-primary" : "btn-secondary"}`}
 | 
				
			||||||
              onClick={() => setActiveTab("env")}
 | 
					              onClick={() => setActiveTab("env")}
 | 
				
			||||||
@@ -1087,6 +1146,172 @@ function App() {
 | 
				
			|||||||
            </>
 | 
					            </>
 | 
				
			||||||
          )}
 | 
					          )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          {activeTab === "settings" && (
 | 
				
			||||||
 | 
					            <>
 | 
				
			||||||
 | 
					              <h3>設定管理</h3>
 | 
				
			||||||
 | 
					              <p style={{ marginBottom: "20px", color: "#7f8c8d" }}>
 | 
				
			||||||
 | 
					                システム設定を管理できます。設定はデータベースに保存され、アプリケーション全体に適用されます。
 | 
				
			||||||
 | 
					              </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <div style={{ marginBottom: "20px" }}>
 | 
				
			||||||
 | 
					                <div className="stats-grid" style={{ gridTemplateColumns: "repeat(4, 1fr)" }}>
 | 
				
			||||||
 | 
					                  <div className="stat-card">
 | 
				
			||||||
 | 
					                    <div className="value">{settings.length}</div>
 | 
				
			||||||
 | 
					                    <div className="label">総設定数</div>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div className="stat-card">
 | 
				
			||||||
 | 
					                    <div className="value">
 | 
				
			||||||
 | 
					                      {settings.filter(s => s.value !== null && s.value !== "").length}
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    <div className="label">設定済み</div>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div className="stat-card">
 | 
				
			||||||
 | 
					                    <div className="value">
 | 
				
			||||||
 | 
					                      {settings.filter(s => s.required).length}
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    <div className="label">必須設定</div>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div className="stat-card">
 | 
				
			||||||
 | 
					                    <div className="value">
 | 
				
			||||||
 | 
					                      {settings.filter(s => s.isCredential).length}
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    <div className="label">認証情報</div>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <div className="settings-list">
 | 
				
			||||||
 | 
					                {settings.map((setting) => (
 | 
				
			||||||
 | 
					                  <div key={setting.key} className="setting-item" style={{ 
 | 
				
			||||||
 | 
					                    display: "flex", 
 | 
				
			||||||
 | 
					                    alignItems: "center",
 | 
				
			||||||
 | 
					                    padding: "16px",
 | 
				
			||||||
 | 
					                    border: "1px solid #ddd",
 | 
				
			||||||
 | 
					                    borderRadius: "4px",
 | 
				
			||||||
 | 
					                    marginBottom: "12px",
 | 
				
			||||||
 | 
					                    backgroundColor: setting.required && (!setting.value || setting.value === "") ? "#fff5f5" : "#fff"
 | 
				
			||||||
 | 
					                  }}>
 | 
				
			||||||
 | 
					                    <div style={{ flex: 1 }}>
 | 
				
			||||||
 | 
					                      <div style={{ display: "flex", alignItems: "center", gap: "8px", marginBottom: "4px" }}>
 | 
				
			||||||
 | 
					                        <h4 style={{ margin: 0, fontSize: "16px" }}>{setting.key}</h4>
 | 
				
			||||||
 | 
					                        {setting.required && (
 | 
				
			||||||
 | 
					                          <span style={{
 | 
				
			||||||
 | 
					                            padding: "2px 6px",
 | 
				
			||||||
 | 
					                            background: "#dc3545",
 | 
				
			||||||
 | 
					                            color: "white",
 | 
				
			||||||
 | 
					                            borderRadius: "4px",
 | 
				
			||||||
 | 
					                            fontSize: "10px"
 | 
				
			||||||
 | 
					                          }}>必須</span>
 | 
				
			||||||
 | 
					                        )}
 | 
				
			||||||
 | 
					                        {setting.isCredential && (
 | 
				
			||||||
 | 
					                          <span style={{
 | 
				
			||||||
 | 
					                            padding: "2px 6px",
 | 
				
			||||||
 | 
					                            background: "#ffc107",
 | 
				
			||||||
 | 
					                            color: "black",
 | 
				
			||||||
 | 
					                            borderRadius: "4px",
 | 
				
			||||||
 | 
					                            fontSize: "10px"
 | 
				
			||||||
 | 
					                          }}>認証情報</span>
 | 
				
			||||||
 | 
					                        )}
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                      <p style={{ margin: "0 0 8px 0", fontSize: "14px", color: "#666" }}>
 | 
				
			||||||
 | 
					                        {setting.description}
 | 
				
			||||||
 | 
					                      </p>
 | 
				
			||||||
 | 
					                      <div style={{ fontSize: "12px", color: "#999" }}>
 | 
				
			||||||
 | 
					                        デフォルト値: {setting.defaultValue || "なし"} | 
 | 
				
			||||||
 | 
					                        最終更新: {new Date(setting.updatedAt).toLocaleString("ja-JP")}
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                      
 | 
				
			||||||
 | 
					                      {editingSettings[setting.key] !== undefined ? (
 | 
				
			||||||
 | 
					                        <div style={{ marginTop: "8px", display: "flex", gap: "8px", alignItems: "center" }}>
 | 
				
			||||||
 | 
					                          <input
 | 
				
			||||||
 | 
					                            type={setting.isCredential ? "password" : "text"}
 | 
				
			||||||
 | 
					                            value={editingSettings[setting.key]}
 | 
				
			||||||
 | 
					                            onChange={(e) => updateEditingValue(setting.key, e.target.value)}
 | 
				
			||||||
 | 
					                            placeholder={setting.defaultValue || "値を入力..."}
 | 
				
			||||||
 | 
					                            style={{
 | 
				
			||||||
 | 
					                              flex: 1,
 | 
				
			||||||
 | 
					                              padding: "6px 10px",
 | 
				
			||||||
 | 
					                              border: "1px solid #ddd",
 | 
				
			||||||
 | 
					                              borderRadius: "4px",
 | 
				
			||||||
 | 
					                              fontSize: "14px"
 | 
				
			||||||
 | 
					                            }}
 | 
				
			||||||
 | 
					                          />
 | 
				
			||||||
 | 
					                          <button
 | 
				
			||||||
 | 
					                            className="btn btn-success"
 | 
				
			||||||
 | 
					                            onClick={() => updateSetting(setting.key, editingSettings[setting.key])}
 | 
				
			||||||
 | 
					                            style={{ fontSize: "12px", padding: "6px 12px" }}
 | 
				
			||||||
 | 
					                          >
 | 
				
			||||||
 | 
					                            保存
 | 
				
			||||||
 | 
					                          </button>
 | 
				
			||||||
 | 
					                          <button
 | 
				
			||||||
 | 
					                            className="btn btn-secondary"
 | 
				
			||||||
 | 
					                            onClick={() => cancelEditingSetting(setting.key)}
 | 
				
			||||||
 | 
					                            style={{ fontSize: "12px", padding: "6px 12px" }}
 | 
				
			||||||
 | 
					                          >
 | 
				
			||||||
 | 
					                            キャンセル
 | 
				
			||||||
 | 
					                          </button>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                      ) : (
 | 
				
			||||||
 | 
					                        <div style={{ marginTop: "8px", display: "flex", alignItems: "center", gap: "12px" }}>
 | 
				
			||||||
 | 
					                          <div style={{
 | 
				
			||||||
 | 
					                            flex: 1,
 | 
				
			||||||
 | 
					                            padding: "6px 10px",
 | 
				
			||||||
 | 
					                            background: "#f8f9fa",
 | 
				
			||||||
 | 
					                            border: "1px solid #e9ecef",
 | 
				
			||||||
 | 
					                            borderRadius: "4px",
 | 
				
			||||||
 | 
					                            fontSize: "14px",
 | 
				
			||||||
 | 
					                            minHeight: "32px",
 | 
				
			||||||
 | 
					                            display: "flex",
 | 
				
			||||||
 | 
					                            alignItems: "center"
 | 
				
			||||||
 | 
					                          }}>
 | 
				
			||||||
 | 
					                            {setting.value === null || setting.value === "" ? (
 | 
				
			||||||
 | 
					                              <span style={{ color: "#dc3545", fontStyle: "italic" }}>未設定</span>
 | 
				
			||||||
 | 
					                            ) : setting.isCredential ? (
 | 
				
			||||||
 | 
					                              <span style={{ color: "#666" }}>••••••••</span>
 | 
				
			||||||
 | 
					                            ) : (
 | 
				
			||||||
 | 
					                              setting.value
 | 
				
			||||||
 | 
					                            )}
 | 
				
			||||||
 | 
					                          </div>
 | 
				
			||||||
 | 
					                          <button
 | 
				
			||||||
 | 
					                            className="btn btn-primary"
 | 
				
			||||||
 | 
					                            onClick={() => startEditingSetting(setting.key, setting.value)}
 | 
				
			||||||
 | 
					                            style={{ fontSize: "12px", padding: "6px 12px" }}
 | 
				
			||||||
 | 
					                          >
 | 
				
			||||||
 | 
					                            編集
 | 
				
			||||||
 | 
					                          </button>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                      )}
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                ))}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <div
 | 
				
			||||||
 | 
					                style={{
 | 
				
			||||||
 | 
					                  marginTop: "20px",
 | 
				
			||||||
 | 
					                  padding: "16px",
 | 
				
			||||||
 | 
					                  background: "#f8f9fa",
 | 
				
			||||||
 | 
					                  borderRadius: "4px",
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <h4>設定について</h4>
 | 
				
			||||||
 | 
					                <ul
 | 
				
			||||||
 | 
					                  style={{
 | 
				
			||||||
 | 
					                    fontSize: "14px",
 | 
				
			||||||
 | 
					                    color: "#6c757d",
 | 
				
			||||||
 | 
					                    marginTop: "8px",
 | 
				
			||||||
 | 
					                    paddingLeft: "20px",
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <li><strong>必須設定:</strong> アプリケーションの動作に必要な設定です</li>
 | 
				
			||||||
 | 
					                  <li><strong>認証情報:</strong> セキュリティ上重要な情報で、表示時にマスクされます</li>
 | 
				
			||||||
 | 
					                  <li>設定の変更は即座にアプリケーションに反映されます</li>
 | 
				
			||||||
 | 
					                  <li>デフォルト値が設定されている項目は、空にするとデフォルト値が使用されます</li>
 | 
				
			||||||
 | 
					                </ul>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          {activeTab === "env" && (
 | 
					          {activeTab === "env" && (
 | 
				
			||||||
            <>
 | 
					            <>
 | 
				
			||||||
              <h3>環境変数設定</h3>
 | 
					              <h3>環境変数設定</h3>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,12 +14,16 @@ import {
 | 
				
			|||||||
  fetchEpisodesWithArticles,
 | 
					  fetchEpisodesWithArticles,
 | 
				
			||||||
  getAllCategories,
 | 
					  getAllCategories,
 | 
				
			||||||
  getAllFeedsIncludingInactive,
 | 
					  getAllFeedsIncludingInactive,
 | 
				
			||||||
 | 
					  getAllSettings,
 | 
				
			||||||
  getFeedByUrl,
 | 
					  getFeedByUrl,
 | 
				
			||||||
  getFeedRequests,
 | 
					  getFeedRequests,
 | 
				
			||||||
  getFeedsByCategory,
 | 
					  getFeedsByCategory,
 | 
				
			||||||
  getFeedsGroupedByCategory,
 | 
					  getFeedsGroupedByCategory,
 | 
				
			||||||
 | 
					  getSetting,
 | 
				
			||||||
 | 
					  getSettingsForAdminUI,
 | 
				
			||||||
  toggleFeedActive,
 | 
					  toggleFeedActive,
 | 
				
			||||||
  updateFeedRequestStatus,
 | 
					  updateFeedRequestStatus,
 | 
				
			||||||
 | 
					  updateSetting,
 | 
				
			||||||
} from "./services/database.js";
 | 
					} from "./services/database.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Validate configuration on startup
 | 
					// Validate configuration on startup
 | 
				
			||||||
@@ -92,6 +96,74 @@ app.get("/api/admin/env", async (c) => {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Settings management API endpoints
 | 
				
			||||||
 | 
					app.get("/api/admin/settings", async (c) => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const settings = await getSettingsForAdminUI();
 | 
				
			||||||
 | 
					    return c.json(settings);
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error("Error fetching settings:", error);
 | 
				
			||||||
 | 
					    return c.json({ error: "Failed to fetch settings" }, 500);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app.get("/api/admin/settings/:key", async (c) => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const key = c.req.param("key");
 | 
				
			||||||
 | 
					    const setting = await getSetting(key);
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if (!setting) {
 | 
				
			||||||
 | 
					      return c.json({ error: "Setting not found" }, 404);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Mask credential values for security
 | 
				
			||||||
 | 
					    if (setting.isCredential && setting.value) {
 | 
				
			||||||
 | 
					      setting.value = "••••••••";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return c.json(setting);
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error("Error fetching setting:", error);
 | 
				
			||||||
 | 
					    return c.json({ error: "Failed to fetch setting" }, 500);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app.put("/api/admin/settings/:key", async (c) => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const key = c.req.param("key");
 | 
				
			||||||
 | 
					    const { value } = await c.req.json<{ value: string }>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!value && value !== "") {
 | 
				
			||||||
 | 
					      return c.json({ error: "Setting value is required" }, 400);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log(`🔧 Admin updating setting: ${key}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Check if setting exists
 | 
				
			||||||
 | 
					    const existingSetting = await getSetting(key);
 | 
				
			||||||
 | 
					    if (!existingSetting) {
 | 
				
			||||||
 | 
					      return c.json({ error: "Setting not found" }, 404);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const updated = await updateSetting(key, value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (updated) {
 | 
				
			||||||
 | 
					      return c.json({
 | 
				
			||||||
 | 
					        result: "UPDATED",
 | 
				
			||||||
 | 
					        message: "Setting updated successfully",
 | 
				
			||||||
 | 
					        key,
 | 
				
			||||||
 | 
					        // Don't return the value for credentials
 | 
				
			||||||
 | 
					        value: existingSetting.isCredential ? "••••••••" : value,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return c.json({ error: "Failed to update setting" }, 500);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error("Error updating setting:", error);
 | 
				
			||||||
 | 
					    return c.json({ error: "Failed to update setting" }, 500);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Feed management API endpoints
 | 
					// Feed management API endpoints
 | 
				
			||||||
app.get("/api/admin/feeds", async (c) => {
 | 
					app.get("/api/admin/feeds", async (c) => {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										12
									
								
								schema.sql
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								schema.sql
									
									
									
									
									
								
							@@ -68,6 +68,17 @@ CREATE TABLE IF NOT EXISTS feed_requests (
 | 
				
			|||||||
  admin_notes TEXT
 | 
					  admin_notes TEXT
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- Environment variable settings stored in database
 | 
				
			||||||
 | 
					CREATE TABLE IF NOT EXISTS settings (
 | 
				
			||||||
 | 
					  key TEXT PRIMARY KEY,
 | 
				
			||||||
 | 
					  value TEXT,
 | 
				
			||||||
 | 
					  is_credential BOOLEAN DEFAULT 0,
 | 
				
			||||||
 | 
					  description TEXT,
 | 
				
			||||||
 | 
					  default_value TEXT,
 | 
				
			||||||
 | 
					  required BOOLEAN DEFAULT 0,
 | 
				
			||||||
 | 
					  updated_at TEXT NOT NULL
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-- 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);
 | 
				
			||||||
@@ -78,3 +89,4 @@ 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_status ON feed_requests(status);
 | 
				
			||||||
CREATE INDEX IF NOT EXISTS idx_feed_requests_created_at ON feed_requests(created_at);
 | 
					CREATE INDEX IF NOT EXISTS idx_feed_requests_created_at ON feed_requests(created_at);
 | 
				
			||||||
 | 
					CREATE INDEX IF NOT EXISTS idx_settings_is_credential ON settings(is_credential);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -126,86 +126,21 @@ function initializeDatabase(): Database {
 | 
				
			|||||||
  db.exec("PRAGMA cache_size = 1000;");
 | 
					  db.exec("PRAGMA cache_size = 1000;");
 | 
				
			||||||
  db.exec("PRAGMA temp_store = memory;");
 | 
					  db.exec("PRAGMA temp_store = memory;");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Ensure schema is set up - use the complete schema
 | 
					  // Load and execute schema from file
 | 
				
			||||||
  db.exec(`CREATE TABLE IF NOT EXISTS feeds (
 | 
					  const schemaPath = path.join(config.paths.projectRoot, "schema.sql");
 | 
				
			||||||
    id TEXT PRIMARY KEY,
 | 
					  if (fs.existsSync(schemaPath)) {
 | 
				
			||||||
    url TEXT NOT NULL UNIQUE,
 | 
					    const schema = fs.readFileSync(schemaPath, "utf-8");
 | 
				
			||||||
    title TEXT,
 | 
					    db.exec(schema);
 | 
				
			||||||
    description TEXT,
 | 
					  } else {
 | 
				
			||||||
    category TEXT,
 | 
					    throw new Error(`Schema file not found: ${schemaPath}`);
 | 
				
			||||||
    last_updated TEXT,
 | 
					  }
 | 
				
			||||||
    created_at TEXT NOT NULL,
 | 
					 | 
				
			||||||
    active BOOLEAN DEFAULT 1
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  CREATE TABLE IF NOT EXISTS articles (
 | 
					 | 
				
			||||||
    id TEXT PRIMARY KEY,
 | 
					 | 
				
			||||||
    feed_id TEXT NOT NULL,
 | 
					 | 
				
			||||||
    title TEXT NOT NULL,
 | 
					 | 
				
			||||||
    link TEXT NOT NULL UNIQUE,
 | 
					 | 
				
			||||||
    description TEXT,
 | 
					 | 
				
			||||||
    content TEXT,
 | 
					 | 
				
			||||||
    pub_date TEXT NOT NULL,
 | 
					 | 
				
			||||||
    discovered_at TEXT NOT NULL,
 | 
					 | 
				
			||||||
    processed BOOLEAN DEFAULT 0,
 | 
					 | 
				
			||||||
    FOREIGN KEY(feed_id) REFERENCES feeds(id)
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  CREATE TABLE IF NOT EXISTS episodes (
 | 
					 | 
				
			||||||
    id TEXT PRIMARY KEY,
 | 
					 | 
				
			||||||
    article_id TEXT NOT NULL,
 | 
					 | 
				
			||||||
    title TEXT NOT NULL,
 | 
					 | 
				
			||||||
    description TEXT,
 | 
					 | 
				
			||||||
    audio_path TEXT NOT NULL,
 | 
					 | 
				
			||||||
    duration INTEGER,
 | 
					 | 
				
			||||||
    file_size INTEGER,
 | 
					 | 
				
			||||||
    category TEXT,
 | 
					 | 
				
			||||||
    created_at TEXT NOT NULL,
 | 
					 | 
				
			||||||
    FOREIGN KEY(article_id) REFERENCES articles(id)
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  CREATE TABLE IF NOT EXISTS processed_feed_items (
 | 
					 | 
				
			||||||
    feed_url TEXT NOT NULL,
 | 
					 | 
				
			||||||
    item_id   TEXT NOT NULL,
 | 
					 | 
				
			||||||
    processed_at TEXT NOT NULL,
 | 
					 | 
				
			||||||
    PRIMARY KEY(feed_url, item_id)
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  CREATE TABLE IF NOT EXISTS tts_queue (
 | 
					 | 
				
			||||||
    id TEXT PRIMARY KEY,
 | 
					 | 
				
			||||||
    item_id TEXT NOT NULL,
 | 
					 | 
				
			||||||
    script_text TEXT NOT NULL,
 | 
					 | 
				
			||||||
    retry_count INTEGER DEFAULT 0,
 | 
					 | 
				
			||||||
    created_at TEXT NOT NULL,
 | 
					 | 
				
			||||||
    last_attempted_at TEXT,
 | 
					 | 
				
			||||||
    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_pub_date ON articles(pub_date);
 | 
					 | 
				
			||||||
  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_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_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);`);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Perform database integrity checks and fixes
 | 
					  // Perform database integrity checks and fixes
 | 
				
			||||||
  performDatabaseIntegrityFixes(db);
 | 
					  performDatabaseIntegrityFixes(db);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Initialize settings table with default values
 | 
				
			||||||
 | 
					  initializeSettings();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // ALTER
 | 
					  // ALTER
 | 
				
			||||||
  // ALTER TABLE feeds ADD COLUMN category TEXT DEFAULT NULL;
 | 
					  // ALTER TABLE feeds ADD COLUMN category TEXT DEFAULT NULL;
 | 
				
			||||||
  // Ensure the category column exists in feeds
 | 
					  // Ensure the category column exists in feeds
 | 
				
			||||||
@@ -1666,6 +1601,267 @@ export async function getEpisodeCategoryMigrationStatus(): Promise<{
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Settings management functions
 | 
				
			||||||
 | 
					export interface Setting {
 | 
				
			||||||
 | 
					  key: string;
 | 
				
			||||||
 | 
					  value: string | null;
 | 
				
			||||||
 | 
					  isCredential: boolean;
 | 
				
			||||||
 | 
					  description: string;
 | 
				
			||||||
 | 
					  defaultValue: string;
 | 
				
			||||||
 | 
					  required: boolean;
 | 
				
			||||||
 | 
					  updatedAt: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function initializeSettings(): Promise<void> {
 | 
				
			||||||
 | 
					  const defaultSettings: Omit<Setting, "updatedAt">[] = [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      key: "OPENAI_API_KEY",
 | 
				
			||||||
 | 
					      value: null,
 | 
				
			||||||
 | 
					      isCredential: true,
 | 
				
			||||||
 | 
					      description: "OpenAI API Key for content generation",
 | 
				
			||||||
 | 
					      defaultValue: "",
 | 
				
			||||||
 | 
					      required: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      key: "OPENAI_API_ENDPOINT", 
 | 
				
			||||||
 | 
					      value: null,
 | 
				
			||||||
 | 
					      isCredential: false,
 | 
				
			||||||
 | 
					      description: "OpenAI API Endpoint URL",
 | 
				
			||||||
 | 
					      defaultValue: "https://api.openai.com/v1",
 | 
				
			||||||
 | 
					      required: false,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      key: "OPENAI_MODEL_NAME",
 | 
				
			||||||
 | 
					      value: null,
 | 
				
			||||||
 | 
					      isCredential: false,
 | 
				
			||||||
 | 
					      description: "OpenAI Model Name",
 | 
				
			||||||
 | 
					      defaultValue: "gpt-4o-mini",
 | 
				
			||||||
 | 
					      required: false,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      key: "VOICEVOX_HOST",
 | 
				
			||||||
 | 
					      value: null,
 | 
				
			||||||
 | 
					      isCredential: false,
 | 
				
			||||||
 | 
					      description: "VOICEVOX Server Host URL",
 | 
				
			||||||
 | 
					      defaultValue: "http://localhost:50021",
 | 
				
			||||||
 | 
					      required: false,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      key: "VOICEVOX_STYLE_ID",
 | 
				
			||||||
 | 
					      value: null,
 | 
				
			||||||
 | 
					      isCredential: false,
 | 
				
			||||||
 | 
					      description: "VOICEVOX Voice Style ID",
 | 
				
			||||||
 | 
					      defaultValue: "0",
 | 
				
			||||||
 | 
					      required: false,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      key: "PODCAST_TITLE",
 | 
				
			||||||
 | 
					      value: null,
 | 
				
			||||||
 | 
					      isCredential: false,
 | 
				
			||||||
 | 
					      description: "Podcast Title",
 | 
				
			||||||
 | 
					      defaultValue: "自動生成ポッドキャスト",
 | 
				
			||||||
 | 
					      required: false,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      key: "PODCAST_LINK",
 | 
				
			||||||
 | 
					      value: null,
 | 
				
			||||||
 | 
					      isCredential: false,
 | 
				
			||||||
 | 
					      description: "Podcast Link URL",
 | 
				
			||||||
 | 
					      defaultValue: "https://your-domain.com/podcast",
 | 
				
			||||||
 | 
					      required: false,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      key: "PODCAST_DESCRIPTION",
 | 
				
			||||||
 | 
					      value: null,
 | 
				
			||||||
 | 
					      isCredential: false,
 | 
				
			||||||
 | 
					      description: "Podcast Description",
 | 
				
			||||||
 | 
					      defaultValue: "RSSフィードから自動生成された音声ポッドキャスト",
 | 
				
			||||||
 | 
					      required: false,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      key: "PODCAST_LANGUAGE",
 | 
				
			||||||
 | 
					      value: null,
 | 
				
			||||||
 | 
					      isCredential: false,
 | 
				
			||||||
 | 
					      description: "Podcast Language",
 | 
				
			||||||
 | 
					      defaultValue: "ja",
 | 
				
			||||||
 | 
					      required: false,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      key: "PODCAST_AUTHOR",
 | 
				
			||||||
 | 
					      value: null,
 | 
				
			||||||
 | 
					      isCredential: false,
 | 
				
			||||||
 | 
					      description: "Podcast Author",
 | 
				
			||||||
 | 
					      defaultValue: "管理者",
 | 
				
			||||||
 | 
					      required: false,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      key: "PODCAST_CATEGORIES",
 | 
				
			||||||
 | 
					      value: null,
 | 
				
			||||||
 | 
					      isCredential: false,
 | 
				
			||||||
 | 
					      description: "Podcast Categories",
 | 
				
			||||||
 | 
					      defaultValue: "Technology",
 | 
				
			||||||
 | 
					      required: false,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      key: "PODCAST_TTL",
 | 
				
			||||||
 | 
					      value: null,
 | 
				
			||||||
 | 
					      isCredential: false,
 | 
				
			||||||
 | 
					      description: "Podcast TTL",
 | 
				
			||||||
 | 
					      defaultValue: "60",
 | 
				
			||||||
 | 
					      required: false,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      key: "PODCAST_BASE_URL",
 | 
				
			||||||
 | 
					      value: null,
 | 
				
			||||||
 | 
					      isCredential: false,
 | 
				
			||||||
 | 
					      description: "Podcast Base URL",
 | 
				
			||||||
 | 
					      defaultValue: "https://your-domain.com",
 | 
				
			||||||
 | 
					      required: false,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      key: "ADMIN_PORT",
 | 
				
			||||||
 | 
					      value: null,
 | 
				
			||||||
 | 
					      isCredential: false,
 | 
				
			||||||
 | 
					      description: "Admin Panel Port",
 | 
				
			||||||
 | 
					      defaultValue: "3001",
 | 
				
			||||||
 | 
					      required: false,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      key: "ADMIN_USERNAME",
 | 
				
			||||||
 | 
					      value: null,
 | 
				
			||||||
 | 
					      isCredential: true,
 | 
				
			||||||
 | 
					      description: "Admin Panel Username",
 | 
				
			||||||
 | 
					      defaultValue: "",
 | 
				
			||||||
 | 
					      required: false,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      key: "ADMIN_PASSWORD",
 | 
				
			||||||
 | 
					      value: null,
 | 
				
			||||||
 | 
					      isCredential: true,
 | 
				
			||||||
 | 
					      description: "Admin Panel Password",
 | 
				
			||||||
 | 
					      defaultValue: "",
 | 
				
			||||||
 | 
					      required: false,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      key: "DISABLE_INITIAL_BATCH",
 | 
				
			||||||
 | 
					      value: null,
 | 
				
			||||||
 | 
					      isCredential: false,
 | 
				
			||||||
 | 
					      description: "Disable Initial Batch Process",
 | 
				
			||||||
 | 
					      defaultValue: "false",
 | 
				
			||||||
 | 
					      required: false,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      key: "FEED_URLS_FILE",
 | 
				
			||||||
 | 
					      value: null,
 | 
				
			||||||
 | 
					      isCredential: false,
 | 
				
			||||||
 | 
					      description: "Feed URLs File Path",
 | 
				
			||||||
 | 
					      defaultValue: "feed_urls.txt",
 | 
				
			||||||
 | 
					      required: false,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const now = new Date().toISOString();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  for (const setting of defaultSettings) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const stmt = db.prepare(
 | 
				
			||||||
 | 
					        "INSERT OR IGNORE INTO settings (key, value, is_credential, description, default_value, required, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      stmt.run(
 | 
				
			||||||
 | 
					        setting.key,
 | 
				
			||||||
 | 
					        setting.value,
 | 
				
			||||||
 | 
					        setting.isCredential ? 1 : 0,
 | 
				
			||||||
 | 
					        setting.description,
 | 
				
			||||||
 | 
					        setting.defaultValue,
 | 
				
			||||||
 | 
					        setting.required ? 1 : 0,
 | 
				
			||||||
 | 
					        now
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error(`Error initializing setting ${setting.key}:`, error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function getAllSettings(): Promise<Setting[]> {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const stmt = db.prepare("SELECT * FROM settings ORDER BY key");
 | 
				
			||||||
 | 
					    const rows = stmt.all() as any[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return rows.map((row) => ({
 | 
				
			||||||
 | 
					      key: row.key,
 | 
				
			||||||
 | 
					      value: row.value,
 | 
				
			||||||
 | 
					      isCredential: Boolean(row.is_credential),
 | 
				
			||||||
 | 
					      description: row.description,
 | 
				
			||||||
 | 
					      defaultValue: row.default_value,
 | 
				
			||||||
 | 
					      required: Boolean(row.required),
 | 
				
			||||||
 | 
					      updatedAt: row.updated_at,
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error("Error getting all settings:", error);
 | 
				
			||||||
 | 
					    throw error;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function getSetting(key: string): Promise<Setting | null> {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const stmt = db.prepare("SELECT * FROM settings WHERE key = ?");
 | 
				
			||||||
 | 
					    const row = stmt.get(key) as any;
 | 
				
			||||||
 | 
					    if (!row) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      key: row.key,
 | 
				
			||||||
 | 
					      value: row.value,
 | 
				
			||||||
 | 
					      isCredential: Boolean(row.is_credential),
 | 
				
			||||||
 | 
					      description: row.description,
 | 
				
			||||||
 | 
					      defaultValue: row.default_value,
 | 
				
			||||||
 | 
					      required: Boolean(row.required),
 | 
				
			||||||
 | 
					      updatedAt: row.updated_at,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error(`Error getting setting ${key}:`, error);
 | 
				
			||||||
 | 
					    throw error;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function updateSetting(key: string, value: string): Promise<boolean> {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const now = new Date().toISOString();
 | 
				
			||||||
 | 
					    const stmt = db.prepare(
 | 
				
			||||||
 | 
					      "UPDATE settings SET value = ?, updated_at = ? WHERE key = ?"
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const result = stmt.run(value, now, key);
 | 
				
			||||||
 | 
					    return result.changes > 0;
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error(`Error updating setting ${key}:`, error);
 | 
				
			||||||
 | 
					    throw error;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function deleteSetting(key: string): Promise<boolean> {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const stmt = db.prepare("DELETE FROM settings WHERE key = ?");
 | 
				
			||||||
 | 
					    const result = stmt.run(key);
 | 
				
			||||||
 | 
					    return result.changes > 0;
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error(`Error deleting setting ${key}:`, error);
 | 
				
			||||||
 | 
					    throw error;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function getSettingsForAdminUI(): Promise<Setting[]> {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const settings = await getAllSettings();
 | 
				
			||||||
 | 
					    return settings.map((setting) => ({
 | 
				
			||||||
 | 
					      ...setting,
 | 
				
			||||||
 | 
					      // Mask credential values for security
 | 
				
			||||||
 | 
					      value: setting.isCredential && setting.value ? "••••••••" : setting.value,
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error("Error getting settings for admin UI:", error);
 | 
				
			||||||
 | 
					    throw error;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function closeDatabase(): void {
 | 
					export function closeDatabase(): void {
 | 
				
			||||||
  db.close();
 | 
					  db.close();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user