refactor: standardize article header layout in UI and increase worker pool capacity

This commit is contained in:
2026-08-25 23:01:40 -07:00
parent 83c70cc3b9
commit 9bc3423009
6 changed files with 175 additions and 85 deletions
+22 -1
View File
@@ -5,6 +5,7 @@ import (
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"strings"
"syscall" "syscall"
"time" "time"
@@ -49,7 +50,7 @@ func main() {
// 3. Initialize EventBus, WorkerPool, and Action Handlers // 3. Initialize EventBus, WorkerPool, and Action Handlers
bus := events.NewEventBus() bus := events.NewEventBus()
pool := worker.NewPool(database, bus, 5, 100) pool := worker.NewPool(database, bus, 5, 1000)
// Register Core Action Handlers // Register Core Action Handlers
scraper := readability.NewScraper(15 * time.Second) scraper := readability.NewScraper(15 * time.Second)
@@ -68,6 +69,26 @@ func main() {
if contentType == "" { if contentType == "" {
contentType = "text" contentType = "text"
} }
// If key == "tags", merge with existing tags so multiple rules append tags
if key == "tags" {
existingVariant, err := database.GetVariant(entry.ID, "tags")
if err == nil && existingVariant != nil && existingVariant.TextValue != "" {
existingTags := strings.Split(existingVariant.TextValue, ",")
newTags := strings.Split(val, ",")
tagSet := make(map[string]bool)
var merged []string
for _, t := range append(existingTags, newTags...) {
trimmed := strings.TrimSpace(t)
if trimmed != "" && !tagSet[strings.ToLower(trimmed)] {
tagSet[strings.ToLower(trimmed)] = true
merged = append(merged, trimmed)
}
}
val = strings.Join(merged, ", ")
}
}
variant := models.EntryVariant{ variant := models.EntryVariant{
EntryID: entry.ID, EntryID: entry.ID,
VariantKey: key, VariantKey: key,
+3
View File
@@ -60,6 +60,9 @@ func (e *Evaluator) resolveField(field string, entry models.FeedEntry) interface
return entry.URL return entry.URL
case "content", "raw_content", "rss_content": case "content", "raw_content", "rss_content":
var parts []string var parts []string
if entry.Title != "" {
parts = append(parts, entry.Title)
}
if entry.Content != "" { if entry.Content != "" {
parts = append(parts, entry.Content) parts = append(parts, entry.Content)
} }
+14 -2
View File
@@ -77,10 +77,22 @@ func (p *Pool) Stop() {
} }
func (p *Pool) Dispatch(ctx context.Context, action models.RuleAction, entry models.FeedEntry, depth int) { func (p *Pool) Dispatch(ctx context.Context, action models.RuleAction, entry models.FeedEntry, depth int) {
task := Task{Action: action, Entry: entry, Depth: depth}
select { select {
case p.queue <- Task{Action: action, Entry: entry, Depth: depth}: case <-ctx.Done():
return
case p.queue <- task:
return
default: default:
p.log.Warn("Worker pool queue full, task dropped", }
// If queue is full, wait up to 5 seconds for a slot before warning
select {
case <-ctx.Done():
return
case p.queue <- task:
case <-time.After(5 * time.Second):
p.log.Warn("Worker pool queue full, task dropped after timeout",
zap.String("action", action.Type), zap.String("action", action.Type),
zap.String("entry_id", entry.ID), zap.String("entry_id", entry.ID),
) )
+38 -36
View File
@@ -410,45 +410,47 @@ export const ThreePaneView: React.FC<ThreePaneViewProps> = ({
{/* Reading Content */} {/* Reading Content */}
<div className="reader-content-canvas"> <div className="reader-content-canvas">
<div className="reader-source-bar"> <div className="article-header">
<span className="tag-badge"> <div className="article-header-meta">
{selectedEntry.feed_title} <span className="article-feed-badge">
</span> {selectedEntry.feed_title}
<span>•</span> </span>
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}> <span className="article-meta-divider">•</span>
<Clock size={12} />{' '} <span className="article-meta-item">
{new Date(selectedEntry.published_at).toLocaleDateString(undefined, { <Clock size={12} />
month: 'short', {new Date(selectedEntry.published_at).toLocaleDateString(undefined, {
day: 'numeric', month: 'short',
year: 'numeric', day: 'numeric',
})} year: 'numeric',
</span> })}
<span>•</span> </span>
<span>{estimateReadingTime(selectedEntry.content || selectedEntry.raw_content)}</span> <span className="article-meta-divider">•</span>
{selectedEntry.author && ( <span className="article-meta-item">{estimateReadingTime(selectedEntry.content || selectedEntry.raw_content)}</span>
<> {selectedEntry.author && (
<span>•</span> <>
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}> <span className="article-meta-divider">•</span>
<UserIcon size={12} /> {selectedEntry.author} <span className="article-meta-item">
</span> <UserIcon size={12} /> {selectedEntry.author}
</> </span>
</>
)}
</div>
<h1 className="reader-headline">
<a href={selectedEntry.url} target="_blank" rel="noopener noreferrer">
{selectedEntry.title}
</a>
</h1>
{getEntryTags(selectedEntry.variants).length > 0 && (
<div className="article-tags-row">
{getEntryTags(selectedEntry.variants).map((tag) => (
<span key={tag} className="variant-tag-chip">🏷️ {tag}</span>
))}
</div>
)} )}
</div> </div>
{getEntryTags(selectedEntry.variants).length > 0 && (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 5, marginBottom: '0.75rem' }}>
{getEntryTags(selectedEntry.variants).map((tag) => (
<span key={tag} className="variant-tag-chip">🏷️ {tag}</span>
))}
</div>
)}
<h1 className="reader-headline">
<a href={selectedEntry.url} target="_blank" rel="noopener noreferrer">
{selectedEntry.title}
</a>
</h1>
{/* Audio player if TTS variant is selected */} {/* Audio player if TTS variant is selected */}
{selectedVariantKey === 'tts' && ( {selectedVariantKey === 'tts' && (
<div style={{ margin: '1rem 0', padding: '1rem', background: 'var(--bg-tertiary)', borderRadius: '8px' }}> <div style={{ margin: '1rem 0', padding: '1rem', background: 'var(--bg-tertiary)', borderRadius: '8px' }}>
+38 -34
View File
@@ -427,43 +427,47 @@ export const WaterfallView: React.FC<WaterfallViewProps> = ({
{/* Reading Body */} {/* Reading Body */}
<div className="mobile-post-reader-content" ref={readerScrollRef}> <div className="mobile-post-reader-content" ref={readerScrollRef}>
<div className="reader-content-canvas" style={{ padding: 0 }}> <div className="reader-content-canvas" style={{ padding: 0 }}>
<div className="reader-source-bar"> <div className="article-header">
<span className="tag-badge">{selectedEntry.feed_title}</span> <div className="article-header-meta">
<span>•</span> <span className="article-feed-badge">
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}> {selectedEntry.feed_title}
<Clock size={12} />{' '} </span>
{new Date(selectedEntry.published_at).toLocaleDateString(undefined, { <span className="article-meta-divider">•</span>
month: 'short', <span className="article-meta-item">
day: 'numeric', <Clock size={12} />
year: 'numeric', {new Date(selectedEntry.published_at).toLocaleDateString(undefined, {
})} month: 'short',
</span> day: 'numeric',
<span>•</span> year: 'numeric',
<span>{estimateReadingTime(selectedEntry.content || selectedEntry.raw_content)}</span> })}
{selectedEntry.author && ( </span>
<> <span className="article-meta-divider">•</span>
<span>•</span> <span className="article-meta-item">{estimateReadingTime(selectedEntry.content || selectedEntry.raw_content)}</span>
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}> {selectedEntry.author && (
<UserIcon size={12} /> {selectedEntry.author} <>
</span> <span className="article-meta-divider">•</span>
</> <span className="article-meta-item">
<UserIcon size={12} /> {selectedEntry.author}
</span>
</>
)}
</div>
<h1 className="reader-headline">
<a href={selectedEntry.url} target="_blank" rel="noopener noreferrer">
{selectedEntry.title}
</a>
</h1>
{getEntryTags(selectedEntry.variants).length > 0 && (
<div className="article-tags-row">
{getEntryTags(selectedEntry.variants).map((tag) => (
<span key={tag} className="variant-tag-chip">🏷️ {tag}</span>
))}
</div>
)} )}
</div> </div>
{getEntryTags(selectedEntry.variants).length > 0 && (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 5, marginBottom: '0.75rem' }}>
{getEntryTags(selectedEntry.variants).map((tag) => (
<span key={tag} className="variant-tag-chip">🏷️ {tag}</span>
))}
</div>
)}
<h1 className="reader-headline">
<a href={selectedEntry.url} target="_blank" rel="noopener noreferrer">
{selectedEntry.title}
</a>
</h1>
{/* Dynamic Content Variant Rendering */} {/* Dynamic Content Variant Rendering */}
{selectedVariantKey === 'tts' && hasTTS ? ( {selectedVariantKey === 'tts' && hasTTS ? (
<div className="audio-player-container" style={{ margin: '1.5rem 0', padding: '1rem', background: 'var(--bg-card-hover)', borderRadius: '12px' }}> <div className="audio-player-container" style={{ margin: '1.5rem 0', padding: '1rem', background: 'var(--bg-card-hover)', borderRadius: '12px' }}>
+60 -12
View File
@@ -269,16 +269,22 @@ html, body, #root {
.variant-tag-chip { .variant-tag-chip {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 3px; gap: 4px;
padding: 1px 6px; padding: 2px 8px;
border-radius: 3px; border-radius: 9999px;
font-size: 0.66rem; font-size: 0.7rem;
font-weight: 600; font-weight: 500;
background: rgba(16, 185, 129, 0.1); background: rgba(16, 185, 129, 0.08);
color: #34d399; color: #34d399;
border: 1px solid rgba(16, 185, 129, 0.2); border: 1px solid rgba(16, 185, 129, 0.22);
white-space: nowrap; white-space: nowrap;
line-height: 1.5; line-height: 1.45;
transition: all 0.15s ease;
}
.variant-tag-chip:hover {
background: rgba(16, 185, 129, 0.15);
border-color: rgba(16, 185, 129, 0.35);
} }
/* Mobile Dropdown Menu */ /* Mobile Dropdown Menu */
@@ -695,15 +701,57 @@ html, body, #root {
padding: 2rem 1.5rem 5rem 1.5rem; padding: 2rem 1.5rem 5rem 1.5rem;
} }
.reader-source-bar { .article-header {
margin-bottom: 1.75rem;
padding-bottom: 1.25rem;
border-bottom: 1px solid var(--border);
}
.reader-source-bar,
.article-header-meta {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
gap: 0.4rem 0.5rem; gap: 0.5rem 0.65rem;
font-size: 0.78rem; font-size: 0.78rem;
color: var(--text-muted); color: var(--text-muted);
margin-bottom: 0.85rem; margin-bottom: 0.85rem;
line-height: 1.6; line-height: 1.5;
}
.article-feed-badge {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 8px;
border-radius: 6px;
font-size: 0.74rem;
font-weight: 600;
background: var(--bg-card);
color: var(--text-main);
border: 1px solid var(--border);
}
.article-meta-item {
display: inline-flex;
align-items: center;
gap: 4px;
color: var(--text-muted);
font-size: 0.75rem;
}
.article-meta-divider {
color: var(--border-dark, #3f3f46);
font-size: 0.65rem;
opacity: 0.6;
}
.article-tags-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.4rem;
margin-top: 0.85rem;
} }
.reader-headline { .reader-headline {
@@ -712,7 +760,7 @@ html, body, #root {
line-height: 1.28; line-height: 1.28;
letter-spacing: -0.02em; letter-spacing: -0.02em;
color: var(--text-main); color: var(--text-main);
margin-bottom: 1.5rem; margin-bottom: 0.5rem;
} }
.reader-headline a { .reader-headline a {