refactor: standardize article header layout in UI and increase worker pool capacity
This commit is contained in:
+22
-1
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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">
|
||||||
|
<span className="article-feed-badge">
|
||||||
{selectedEntry.feed_title}
|
{selectedEntry.feed_title}
|
||||||
</span>
|
</span>
|
||||||
<span>•</span>
|
<span className="article-meta-divider">•</span>
|
||||||
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
<span className="article-meta-item">
|
||||||
<Clock size={12} />{' '}
|
<Clock size={12} />
|
||||||
{new Date(selectedEntry.published_at).toLocaleDateString(undefined, {
|
{new Date(selectedEntry.published_at).toLocaleDateString(undefined, {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
<span>•</span>
|
<span className="article-meta-divider">•</span>
|
||||||
<span>{estimateReadingTime(selectedEntry.content || selectedEntry.raw_content)}</span>
|
<span className="article-meta-item">{estimateReadingTime(selectedEntry.content || selectedEntry.raw_content)}</span>
|
||||||
{selectedEntry.author && (
|
{selectedEntry.author && (
|
||||||
<>
|
<>
|
||||||
<span>•</span>
|
<span className="article-meta-divider">•</span>
|
||||||
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
<span className="article-meta-item">
|
||||||
<UserIcon size={12} /> {selectedEntry.author}
|
<UserIcon size={12} /> {selectedEntry.author}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</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">
|
<h1 className="reader-headline">
|
||||||
<a href={selectedEntry.url} target="_blank" rel="noopener noreferrer">
|
<a href={selectedEntry.url} target="_blank" rel="noopener noreferrer">
|
||||||
{selectedEntry.title}
|
{selectedEntry.title}
|
||||||
</a>
|
</a>
|
||||||
</h1>
|
</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>
|
||||||
|
|
||||||
{/* 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' }}>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
<span className="article-meta-divider">•</span>
|
||||||
|
<span className="article-meta-item">
|
||||||
|
<Clock size={12} />
|
||||||
{new Date(selectedEntry.published_at).toLocaleDateString(undefined, {
|
{new Date(selectedEntry.published_at).toLocaleDateString(undefined, {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
<span>•</span>
|
<span className="article-meta-divider">•</span>
|
||||||
<span>{estimateReadingTime(selectedEntry.content || selectedEntry.raw_content)}</span>
|
<span className="article-meta-item">{estimateReadingTime(selectedEntry.content || selectedEntry.raw_content)}</span>
|
||||||
{selectedEntry.author && (
|
{selectedEntry.author && (
|
||||||
<>
|
<>
|
||||||
<span>•</span>
|
<span className="article-meta-divider">•</span>
|
||||||
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
<span className="article-meta-item">
|
||||||
<UserIcon size={12} /> {selectedEntry.author}
|
<UserIcon size={12} /> {selectedEntry.author}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</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">
|
<h1 className="reader-headline">
|
||||||
<a href={selectedEntry.url} target="_blank" rel="noopener noreferrer">
|
<a href={selectedEntry.url} target="_blank" rel="noopener noreferrer">
|
||||||
{selectedEntry.title}
|
{selectedEntry.title}
|
||||||
</a>
|
</a>
|
||||||
</h1>
|
</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>
|
||||||
|
|
||||||
{/* 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
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user