Add experimental New to You dashboard section
This commit is contained in:
@@ -263,6 +263,39 @@ def select_items(items:list[dict[str,Any]],prefs:dict[str,Any],max_items:int=14)
|
|||||||
consider(3)
|
consider(3)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
def select_exploration_items(items:list[dict[str,Any]],prefs:dict[str,Any],exclude_urls:set[str]|None=None,max_items:int=4)->list[dict[str,Any]]:
|
||||||
|
now=datetime.now(timezone.utc)
|
||||||
|
exclude_urls=exclude_urls or set()
|
||||||
|
boosted={str(x).strip() for x in prefs.get('boost_categories',[])}
|
||||||
|
quiet={str(x).strip() for x in prefs.get('quiet_categories',[])}
|
||||||
|
preferred=['tech','smart_home','gaming','cooking','security']
|
||||||
|
recent=[i for i in items if (now-parse_time(i.get('published_at',''))).total_seconds()<96*3600]
|
||||||
|
recent.sort(key=lambda i:(score_item(i,prefs),i.get('published_at','')),reverse=True)
|
||||||
|
|
||||||
|
out=[]; seen=set(exclude_urls); categories=Counter(); sources=Counter()
|
||||||
|
|
||||||
|
def consider(allowed_boosted:bool, only_categories:set[str]|None=None, per_category_limit:int=1)->None:
|
||||||
|
for i in recent:
|
||||||
|
if len(out)>=max_items: break
|
||||||
|
url=i.get('url',''); src=i.get('source',''); cat=(i.get('category') or 'other').strip() or 'other'
|
||||||
|
if not url or url in seen or sources[src]>=1 or categories[cat]>=per_category_limit:
|
||||||
|
continue
|
||||||
|
if cat in quiet:
|
||||||
|
continue
|
||||||
|
if only_categories is not None and cat not in only_categories:
|
||||||
|
continue
|
||||||
|
if not allowed_boosted and cat in boosted:
|
||||||
|
continue
|
||||||
|
out.append(i); seen.add(url); categories[cat]+=1; sources[src]+=1
|
||||||
|
|
||||||
|
# Keep this lane genuinely exploratory: preferred off-profile categories first,
|
||||||
|
# then broader off-profile picks, allowing a little depth if the pool is narrow.
|
||||||
|
consider(False, set(preferred), 1)
|
||||||
|
consider(False, set(preferred), 3)
|
||||||
|
consider(False, None, 1)
|
||||||
|
consider(False, None, 3)
|
||||||
|
return out
|
||||||
|
|
||||||
def label(cat:str)->str:
|
def label(cat:str)->str:
|
||||||
e,n=LABELS.get(cat,('🧩',(cat or 'Other').title())); return f'{e} {n}'
|
e,n=LABELS.get(cat,('🧩',(cat or 'Other').title())); return f'{e} {n}'
|
||||||
|
|
||||||
@@ -555,6 +588,7 @@ def render()->str:
|
|||||||
candidates=load_json(DIGEST_ROOT/'data/candidates.json',[]); prefs=load_json(DIGEST_ROOT/'data/preferences.json',{})
|
candidates=load_json(DIGEST_ROOT/'data/candidates.json',[]); prefs=load_json(DIGEST_ROOT/'data/preferences.json',{})
|
||||||
donetick_todos=fetch_donetick_todos(); todos=inject_dashboard_todos(donetick_todos or load_json(DATA/'todos.json',[])); calendar=load_json(DATA/'calendar.json',[]); notes=load_json(DATA/'notes.json',[])
|
donetick_todos=fetch_donetick_todos(); todos=inject_dashboard_todos(donetick_todos or load_json(DATA/'todos.json',[])); calendar=load_json(DATA/'calendar.json',[]); notes=load_json(DATA/'notes.json',[])
|
||||||
selected=select_items(candidates,prefs); top_briefing=selected[:6]; top_urls={i.get('url','') for i in top_briefing if i.get('url')}; groups={'space':[],'homelab':[],'local':[],'signals':[]}
|
selected=select_items(candidates,prefs); top_briefing=selected[:6]; top_urls={i.get('url','') for i in top_briefing if i.get('url')}; groups={'space':[],'homelab':[],'local':[],'signals':[]}
|
||||||
|
exploration=select_exploration_items(candidates,prefs,{i.get('url','') for i in selected if i.get('url')},4)
|
||||||
for i in selected:
|
for i in selected:
|
||||||
if i.get('url','') in top_urls: continue
|
if i.get('url','') in top_urls: continue
|
||||||
cat=i.get('category')
|
cat=i.get('category')
|
||||||
@@ -590,6 +624,7 @@ def render()->str:
|
|||||||
{section('Sky / Space','SpaceX, NASA, planetary science, and astronomy.',groups['space'][:5],'No sky signal right now.',compact=True)}
|
{section('Sky / Space','SpaceX, NASA, planetary science, and astronomy.',groups['space'][:5],'No sky signal right now.',compact=True)}
|
||||||
{section('Homelab / Smart Home / Radio','Infrastructure, self-hosting, smart home, and ham radio.',groups['homelab'][:4],'No homelab signal right now.',compact=True)}
|
{section('Homelab / Smart Home / Radio','Infrastructure, self-hosting, smart home, and ham radio.',groups['homelab'][:4],'No homelab signal right now.',compact=True)}
|
||||||
{section('Local','Clarksville / Middle Tennessee items that seem useful.',groups['local'][:3],'No local signal right now.',compact=True)}
|
{section('Local','Clarksville / Middle Tennessee items that seem useful.',groups['local'][:3],'No local signal right now.',compact=True)}
|
||||||
|
{section('New to You','Intentional oddballs outside your usual top lanes, so you can see if any new categories deserve a slot.',exploration[:4],'No worthwhile experimental picks right now.',compact=True)}
|
||||||
{section('Other signals','Everything else that survived filtering.',groups['signals'][:3],'No extra signals.',compact=True)}
|
{section('Other signals','Everything else that survived filtering.',groups['signals'][:3],'No extra signals.',compact=True)}
|
||||||
<footer><p>Category mix: {esc(', '.join(f'{k}: {v}' for k,v in counts.most_common(10)))}</p><p>Local-only on NOMAD unless John says otherwise. Weather from Open-Meteo; moon phase calculated locally.</p></footer></main>{feedback_bootstrap()}</body></html>"""
|
<footer><p>Category mix: {esc(', '.join(f'{k}: {v}' for k,v in counts.most_common(10)))}</p><p>Local-only on NOMAD unless John says otherwise. Weather from Open-Meteo; moon phase calculated locally.</p></footer></main>{feedback_bootstrap()}</body></html>"""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user