Admin: per-kanal warmup-konfigurasjon
- channels.config får warmup_mode (all/messages/days/none) og warmup_value - Migrasjon setter default til "all" for eksisterende kanaler - Admin-side /admin/channels med oversikt og inline-redigering - API PATCH /api/channels/:id/config for å oppdatere konfig - Worker respekterer per-kanal konfig ved warmup - Sidebar-lenke til kanaler-admin Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8b58d434e9
commit
0d8521855f
6 changed files with 508 additions and 27 deletions
8
migrations/0006_warmup_config.sql
Normal file
8
migrations/0006_warmup_config.sql
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
-- Legg til warmup-innstillinger i channels.config
|
||||||
|
-- Default: warmup_mode = "all", warmup_value = null (last alt)
|
||||||
|
-- Andre moduser: "messages" (siste N), "days" (siste N dager), "none" (ikke last)
|
||||||
|
|
||||||
|
UPDATE channels
|
||||||
|
SET config = config
|
||||||
|
|| '{"warmup_mode": "all", "warmup_value": null}'::jsonb
|
||||||
|
WHERE NOT (config ? 'warmup_mode');
|
||||||
|
|
@ -145,6 +145,7 @@
|
||||||
{/each}
|
{/each}
|
||||||
<li class="nav-divider"></li>
|
<li class="nav-divider"></li>
|
||||||
<li><a href="/admin/pages" onclick={() => (open = false)}>Rediger sider</a></li>
|
<li><a href="/admin/pages" onclick={() => (open = false)}>Rediger sider</a></li>
|
||||||
|
<li><a href="/admin/channels" onclick={() => (open = false)}>Kanaler</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="sidebar-footer">
|
<div class="sidebar-footer">
|
||||||
|
|
|
||||||
33
web/src/routes/admin/channels/+page.server.ts
Normal file
33
web/src/routes/admin/channels/+page.server.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
import { sql } from '$lib/server/db';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ locals }) => {
|
||||||
|
if (!locals.workspace) error(404);
|
||||||
|
|
||||||
|
const channels = await sql`
|
||||||
|
SELECT
|
||||||
|
c.id,
|
||||||
|
c.name,
|
||||||
|
c.config,
|
||||||
|
n.workspace_id,
|
||||||
|
p.name AS parent_name,
|
||||||
|
(SELECT count(*)::int FROM messages m WHERE m.channel_id = c.id) AS message_count,
|
||||||
|
(SELECT max(m.created_at) FROM messages m WHERE m.channel_id = c.id) AS last_message_at
|
||||||
|
FROM channels c
|
||||||
|
JOIN nodes n ON n.id = c.id
|
||||||
|
LEFT JOIN nodes pn ON pn.id = c.parent_id
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT id, COALESCE(
|
||||||
|
(SELECT name FROM channels WHERE id = pn2.id),
|
||||||
|
(SELECT title FROM factoids WHERE id = pn2.id),
|
||||||
|
pn2.node_type
|
||||||
|
) AS name
|
||||||
|
FROM nodes pn2
|
||||||
|
) p ON p.id = c.parent_id
|
||||||
|
WHERE n.workspace_id = ${locals.workspace.id}
|
||||||
|
ORDER BY c.name
|
||||||
|
`;
|
||||||
|
|
||||||
|
return { channels };
|
||||||
|
};
|
||||||
348
web/src/routes/admin/channels/+page.svelte
Normal file
348
web/src/routes/admin/channels/+page.svelte
Normal file
|
|
@ -0,0 +1,348 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
|
let { data } = $props<{ data: PageData }>();
|
||||||
|
|
||||||
|
type WarmupMode = 'all' | 'messages' | 'days' | 'none';
|
||||||
|
|
||||||
|
interface ChannelRow {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
config: Record<string, unknown>;
|
||||||
|
parent_name: string | null;
|
||||||
|
message_count: number;
|
||||||
|
last_message_at: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let channels = $state<ChannelRow[]>(data.channels as ChannelRow[]);
|
||||||
|
let saving = $state<string | null>(null);
|
||||||
|
let saved = $state<string | null>(null);
|
||||||
|
let errorMsg = $state('');
|
||||||
|
|
||||||
|
function getWarmupMode(config: Record<string, unknown>): WarmupMode {
|
||||||
|
return (config.warmup_mode as WarmupMode) ?? 'all';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWarmupValue(config: Record<string, unknown>): number | null {
|
||||||
|
const v = config.warmup_value;
|
||||||
|
return typeof v === 'number' ? v : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(iso: string | null): string {
|
||||||
|
if (!iso) return 'Aldri';
|
||||||
|
const d = new Date(iso);
|
||||||
|
const now = new Date();
|
||||||
|
const diffMs = now.getTime() - d.getTime();
|
||||||
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||||
|
if (diffDays === 0) return 'I dag';
|
||||||
|
if (diffDays === 1) return 'I går';
|
||||||
|
if (diffDays < 30) return `${diffDays} dager siden`;
|
||||||
|
return d.toLocaleDateString('nb-NO', { day: 'numeric', month: 'short', year: 'numeric' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function modeLabel(mode: WarmupMode): string {
|
||||||
|
switch (mode) {
|
||||||
|
case 'all': return 'Alt';
|
||||||
|
case 'messages': return 'Siste N meldinger';
|
||||||
|
case 'days': return 'Siste N dager';
|
||||||
|
case 'none': return 'Ingen';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveConfig(channel: ChannelRow) {
|
||||||
|
saving = channel.id;
|
||||||
|
errorMsg = '';
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/channels/${channel.id}/config`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
warmup_mode: getWarmupMode(channel.config),
|
||||||
|
warmup_value: getWarmupValue(channel.config)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Feil ved lagring');
|
||||||
|
channel.config = await res.json();
|
||||||
|
saved = channel.id;
|
||||||
|
setTimeout(() => { if (saved === channel.id) saved = null; }, 2000);
|
||||||
|
} catch {
|
||||||
|
errorMsg = `Kunne ikke lagre konfig for ${channel.name}`;
|
||||||
|
} finally {
|
||||||
|
saving = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMode(channel: ChannelRow, mode: WarmupMode) {
|
||||||
|
channel.config = { ...channel.config, warmup_mode: mode };
|
||||||
|
if (mode === 'messages' && getWarmupValue(channel.config) === null) {
|
||||||
|
channel.config = { ...channel.config, warmup_value: 100 };
|
||||||
|
} else if (mode === 'days' && getWarmupValue(channel.config) === null) {
|
||||||
|
channel.config = { ...channel.config, warmup_value: 30 };
|
||||||
|
} else if (mode === 'all' || mode === 'none') {
|
||||||
|
channel.config = { ...channel.config, warmup_value: null };
|
||||||
|
}
|
||||||
|
saveConfig(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setValue(channel: ChannelRow, value: number) {
|
||||||
|
channel.config = { ...channel.config, warmup_value: value };
|
||||||
|
saveConfig(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalMessages = $derived(channels.reduce((sum, c) => sum + c.message_count, 0));
|
||||||
|
let activeChannels = $derived(channels.filter(c => getWarmupMode(c.config) !== 'none').length);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="admin-channels">
|
||||||
|
<div class="header">
|
||||||
|
<h2>Kanaler</h2>
|
||||||
|
<span class="header-stats">{activeChannels} aktive / {channels.length} totalt / {totalMessages} meldinger</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if errorMsg}
|
||||||
|
<div class="error-msg">{errorMsg}</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="channel-list">
|
||||||
|
<div class="channel-row channel-row--header">
|
||||||
|
<span class="col-name">Kanal</span>
|
||||||
|
<span class="col-parent">Tilhører</span>
|
||||||
|
<span class="col-count">Meldinger</span>
|
||||||
|
<span class="col-last">Siste aktivitet</span>
|
||||||
|
<span class="col-warmup">Warmup</span>
|
||||||
|
<span class="col-value">Verdi</span>
|
||||||
|
<span class="col-status"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#each channels as channel (channel.id)}
|
||||||
|
{@const mode = getWarmupMode(channel.config)}
|
||||||
|
{@const value = getWarmupValue(channel.config)}
|
||||||
|
<div class="channel-row" class:channel-row--inactive={mode === 'none'}>
|
||||||
|
<span class="col-name" title={channel.id}>{channel.name}</span>
|
||||||
|
<span class="col-parent">{channel.parent_name ?? '—'}</span>
|
||||||
|
<span class="col-count">{channel.message_count}</span>
|
||||||
|
<span class="col-last">{formatDate(channel.last_message_at)}</span>
|
||||||
|
<span class="col-warmup">
|
||||||
|
<select
|
||||||
|
value={mode}
|
||||||
|
onchange={(e) => setMode(channel, (e.target as HTMLSelectElement).value as WarmupMode)}
|
||||||
|
>
|
||||||
|
<option value="all">Alt</option>
|
||||||
|
<option value="messages">Siste N meldinger</option>
|
||||||
|
<option value="days">Siste N dager</option>
|
||||||
|
<option value="none">Ingen</option>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
<span class="col-value">
|
||||||
|
{#if mode === 'messages' || mode === 'days'}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={value ?? ''}
|
||||||
|
onchange={(e) => setValue(channel, parseInt((e.target as HTMLInputElement).value) || 100)}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<span class="col-value-placeholder">{mode === 'all' ? 'Alt' : '—'}</span>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
<span class="col-status">
|
||||||
|
{#if saving === channel.id}
|
||||||
|
<span class="status-saving">...</span>
|
||||||
|
{:else if saved === channel.id}
|
||||||
|
<span class="status-saved">OK</span>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if channels.length === 0}
|
||||||
|
<p class="hint">Ingen kanaler funnet i dette workspacet.</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<strong>Warmup-moduser</strong>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Alt</strong> — laster alle meldinger i kanalen inn i SpacetimeDB ved oppstart</li>
|
||||||
|
<li><strong>Siste N meldinger</strong> — laster kun de N nyeste meldingene</li>
|
||||||
|
<li><strong>Siste N dager</strong> — laster meldinger fra de siste N dagene</li>
|
||||||
|
<li><strong>Ingen</strong> — kanalen lastes ikke inn (arkivert/inaktiv)</li>
|
||||||
|
</ul>
|
||||||
|
<p>Endringer trer i kraft ved neste restart av worker.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.admin-channels {
|
||||||
|
max-width: 900px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-stats {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #8b92a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-msg {
|
||||||
|
background: #3b1219;
|
||||||
|
border: 1px solid #6b2028;
|
||||||
|
color: #f87171;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
background: #2d3148;
|
||||||
|
border: 1px solid #2d3148;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 1.5fr 80px 120px 160px 80px 40px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
background: #161822;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-row--header {
|
||||||
|
background: #1a1d2e;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #8b92a5;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-row--inactive {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-name {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-parent {
|
||||||
|
color: #8b92a5;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-count {
|
||||||
|
text-align: right;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-last {
|
||||||
|
color: #8b92a5;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-value-placeholder {
|
||||||
|
color: #8b92a5;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
background: #0f1117;
|
||||||
|
border: 1px solid #2d3148;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #e1e4e8;
|
||||||
|
padding: 0.25rem 0.35rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="number"] {
|
||||||
|
background: #0f1117;
|
||||||
|
border: 1px solid #2d3148;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #e1e4e8;
|
||||||
|
padding: 0.25rem 0.35rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
select:focus, input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-saving {
|
||||||
|
color: #8b92a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-saved {
|
||||||
|
color: #4ade80;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
color: #8b92a5;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
background: #161822;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
background: #161822;
|
||||||
|
border: 1px solid #2d3148;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #8b92a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box strong {
|
||||||
|
color: #e1e4e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box ul {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box li {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box p {
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.channel-row {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
.channel-row--header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.col-parent, .col-last {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
30
web/src/routes/api/channels/[id]/config/+server.ts
Normal file
30
web/src/routes/api/channels/[id]/config/+server.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { json, error } from '@sveltejs/kit';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
import { sql } from '$lib/server/db';
|
||||||
|
|
||||||
|
/** PATCH /api/channels/:id/config — Oppdater kanal-konfig (merge) */
|
||||||
|
export const PATCH: RequestHandler = async ({ params, request, locals }) => {
|
||||||
|
if (!locals.workspace || !locals.user) error(401);
|
||||||
|
|
||||||
|
const updates = await request.json();
|
||||||
|
if (!updates || typeof updates !== 'object') error(400, 'Ugyldig config');
|
||||||
|
|
||||||
|
// Verifiser at kanalen tilhører workspace
|
||||||
|
const [channel] = await sql`
|
||||||
|
SELECT c.id, c.config
|
||||||
|
FROM channels c
|
||||||
|
JOIN nodes n ON n.id = c.id
|
||||||
|
WHERE c.id = ${params.id}::uuid AND n.workspace_id = ${locals.workspace.id}
|
||||||
|
`;
|
||||||
|
if (!channel) error(404, 'Kanal ikke funnet');
|
||||||
|
|
||||||
|
// Merge oppdateringer inn i eksisterende config
|
||||||
|
await sql`
|
||||||
|
UPDATE channels
|
||||||
|
SET config = config || ${sql.json(updates)}
|
||||||
|
WHERE id = ${params.id}::uuid
|
||||||
|
`;
|
||||||
|
|
||||||
|
const [updated] = await sql`SELECT config FROM channels WHERE id = ${params.id}::uuid`;
|
||||||
|
return json(updated.config);
|
||||||
|
};
|
||||||
|
|
@ -1,45 +1,92 @@
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
use serde::Deserialize;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
/// Oppvarming: les siste N meldinger per aktive kanal fra PG og last inn i SpacetimeDB.
|
#[derive(Deserialize)]
|
||||||
|
struct ChannelConfig {
|
||||||
|
#[serde(default = "default_warmup_mode")]
|
||||||
|
warmup_mode: String,
|
||||||
|
warmup_value: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_warmup_mode() -> String {
|
||||||
|
"all".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ChannelInfo {
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
config: ChannelConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Oppvarming: les meldinger per kanal fra PG og last inn i SpacetimeDB.
|
||||||
|
/// Respekterer per-kanal warmup-konfigurasjon.
|
||||||
pub async fn run(
|
pub async fn run(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
http: &Client,
|
http: &Client,
|
||||||
spacetimedb_url: &str,
|
spacetimedb_url: &str,
|
||||||
module: &str,
|
module: &str,
|
||||||
limit: i64,
|
default_limit: i64,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
info!(limit, "Starter oppvarming (PG → SpacetimeDB)");
|
info!("Starter oppvarming (PG → SpacetimeDB)");
|
||||||
|
|
||||||
// Finn aktive kanaler (kanaler med meldinger)
|
// Hent kanaler med konfig
|
||||||
let channels: Vec<(String,)> = sqlx::query_as(
|
let rows: Vec<(String, String, serde_json::Value)> = sqlx::query_as(
|
||||||
"SELECT DISTINCT channel_id::text FROM messages WHERE channel_id IS NOT NULL"
|
"SELECT c.id::text, c.name, c.config FROM channels c JOIN nodes n ON n.id = c.id"
|
||||||
)
|
)
|
||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let channels: Vec<ChannelInfo> = rows.into_iter().map(|(id, name, config_val)| {
|
||||||
|
let config: ChannelConfig = serde_json::from_value(config_val)
|
||||||
|
.unwrap_or(ChannelConfig { warmup_mode: default_warmup_mode(), warmup_value: None });
|
||||||
|
ChannelInfo { id, name, config }
|
||||||
|
}).collect();
|
||||||
|
|
||||||
if channels.is_empty() {
|
if channels.is_empty() {
|
||||||
info!("Ingen aktive kanaler funnet — oppvarming fullført");
|
info!("Ingen kanaler funnet — oppvarming fullført");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
info!(channels = channels.len(), "Aktive kanaler funnet");
|
let active: Vec<&ChannelInfo> = channels.iter()
|
||||||
|
.filter(|c| c.config.warmup_mode != "none")
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
info!(
|
||||||
|
total = channels.len(),
|
||||||
|
active = active.len(),
|
||||||
|
skipped = channels.len() - active.len(),
|
||||||
|
"Kanaler funnet"
|
||||||
|
);
|
||||||
|
|
||||||
let mut total_messages = 0u64;
|
let mut total_messages = 0u64;
|
||||||
let mut total_reactions = 0u64;
|
let mut total_reactions = 0u64;
|
||||||
|
|
||||||
for (channel_id,) in &channels {
|
for ch in &active {
|
||||||
// Rydd kanalen i SpacetimeDB først for å unngå duplikater
|
// Rydd kanalen i SpacetimeDB først
|
||||||
if let Err(e) = call_reducer(http, spacetimedb_url, module, "clear_channel", &serde_json::json!({
|
if let Err(e) = call_reducer(http, spacetimedb_url, module, "clear_channel", &serde_json::json!({
|
||||||
"channel_id": channel_id
|
"channel_id": ch.id
|
||||||
})).await {
|
})).await {
|
||||||
warn!(channel_id, error = %e, "Kunne ikke rydde kanal — hopper over");
|
warn!(channel = %ch.name, error = %e, "Kunne ikke rydde kanal — hopper over");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hent meldinger med forfatterinfo
|
// Bygg WHERE-clause basert på warmup-modus
|
||||||
let rows: Vec<(String, String, String, String, String, String, String, Option<String>, String)> = sqlx::query_as(
|
let (where_clause, limit) = match ch.config.warmup_mode.as_str() {
|
||||||
|
"messages" => {
|
||||||
|
let n = ch.config.warmup_value.unwrap_or(default_limit);
|
||||||
|
(String::new(), n)
|
||||||
|
},
|
||||||
|
"days" => {
|
||||||
|
let days = ch.config.warmup_value.unwrap_or(30);
|
||||||
|
(format!("AND m.created_at >= now() - interval '{} days'", days), i64::MAX)
|
||||||
|
},
|
||||||
|
// "all" og alt annet
|
||||||
|
_ => (String::new(), i64::MAX),
|
||||||
|
};
|
||||||
|
|
||||||
|
let query = format!(
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
m.id::text,
|
m.id::text,
|
||||||
|
|
@ -54,17 +101,24 @@ pub async fn run(
|
||||||
FROM messages m
|
FROM messages m
|
||||||
JOIN nodes n ON n.id = m.id
|
JOIN nodes n ON n.id = m.id
|
||||||
LEFT JOIN users u ON u.authentik_id = m.author_id
|
LEFT JOIN users u ON u.authentik_id = m.author_id
|
||||||
WHERE m.channel_id = $1::uuid
|
WHERE m.channel_id = $1::uuid {}
|
||||||
ORDER BY m.created_at DESC
|
ORDER BY m.created_at DESC
|
||||||
LIMIT $2
|
LIMIT $2
|
||||||
"#
|
"#,
|
||||||
)
|
where_clause
|
||||||
.bind(channel_id)
|
);
|
||||||
|
|
||||||
|
let rows: Vec<(String, String, String, String, String, String, String, Option<String>, String)> =
|
||||||
|
sqlx::query_as(&query)
|
||||||
|
.bind(&ch.id)
|
||||||
.bind(limit)
|
.bind(limit)
|
||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if rows.is_empty() { continue; }
|
if rows.is_empty() {
|
||||||
|
info!(channel = %ch.name, mode = %ch.config.warmup_mode, "Ingen meldinger å laste");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Bygg JSON-array
|
// Bygg JSON-array
|
||||||
let messages: Vec<serde_json::Value> = rows.iter().map(|r| {
|
let messages: Vec<serde_json::Value> = rows.iter().map(|r| {
|
||||||
|
|
@ -87,7 +141,7 @@ pub async fn run(
|
||||||
if let Err(e) = call_reducer(http, spacetimedb_url, module, "load_messages", &serde_json::json!({
|
if let Err(e) = call_reducer(http, spacetimedb_url, module, "load_messages", &serde_json::json!({
|
||||||
"messages_json": json_str
|
"messages_json": json_str
|
||||||
})).await {
|
})).await {
|
||||||
warn!(channel_id, error = %e, "Feil ved lasting av meldinger");
|
warn!(channel = %ch.name, error = %e, "Feil ved lasting av meldinger");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,7 +161,7 @@ pub async fn run(
|
||||||
WHERE m.channel_id = $1::uuid
|
WHERE m.channel_id = $1::uuid
|
||||||
"#
|
"#
|
||||||
)
|
)
|
||||||
.bind(channel_id)
|
.bind(&ch.id)
|
||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
@ -125,17 +179,24 @@ pub async fn run(
|
||||||
if let Err(e) = call_reducer(http, spacetimedb_url, module, "load_reactions", &serde_json::json!({
|
if let Err(e) = call_reducer(http, spacetimedb_url, module, "load_reactions", &serde_json::json!({
|
||||||
"reactions_json": reactions_json
|
"reactions_json": reactions_json
|
||||||
})).await {
|
})).await {
|
||||||
warn!(channel_id, error = %e, "Feil ved lasting av reaksjoner");
|
warn!(channel = %ch.name, error = %e, "Feil ved lasting av reaksjoner");
|
||||||
} else {
|
} else {
|
||||||
total_reactions += reaction_rows.len() as u64;
|
total_reactions += reaction_rows.len() as u64;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
info!(channel_id, messages = count, reactions = reaction_rows.len(), "Kanal oppvarmet");
|
info!(
|
||||||
|
channel = %ch.name,
|
||||||
|
mode = %ch.config.warmup_mode,
|
||||||
|
value = ?ch.config.warmup_value,
|
||||||
|
messages = count,
|
||||||
|
reactions = reaction_rows.len(),
|
||||||
|
"Kanal oppvarmet"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
channels = channels.len(),
|
channels = active.len(),
|
||||||
messages = total_messages,
|
messages = total_messages,
|
||||||
reactions = total_reactions,
|
reactions = total_reactions,
|
||||||
"Oppvarming fullført"
|
"Oppvarming fullført"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue