# dsh-plugin-newsboard An AI & tech news board for DeepSeek Harness. The **0.2.1+** version is an official-style **left sidebar entry + standalone page**: there is no floating top-right ticker and no Settings section any more. Browsing, search, sources, translation and settings all live on the `News Board` page. [δΈ­ζ–‡](README.md) [![dsh-plugin topic](https://img.shields.io/badge/topic-dsh--plugin-blue)](https://github.com/topics/dsh-plugin) [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE) > **Current release: v2.0.0 (DSH 0.2.1+, Web + Desktop).** > `v1.2.0` is the old floating ticker + Settings section version and is **not compatible with 0.2.1**; upgrade from 0.1.x directly to `v2.0.0`. ## Features - **Sidebar standalone page**: a left `News Board` entry (`sidebar.panellist`) plus a keyed `main` page (`newsboard`). The page has a fixed header, tab strip and footer; only the center content scrolls. - **Same bundle on Web and Desktop**: the header shows `🌐 Web` / `πŸ–₯ Desktop`; macOS also gets `--dsh-frame-top-clearance` top padding. Every endpoint goes through an authenticated `/api/dsh-plugin-newsboard/...` Connection route. - **Dynamic category tabs**: presets `AI / Tech / Games / Life`; each category is a name plus N keywords and can be added, renamed or removed in Settings. `All` always shows every fetched story, so a keyword miss never hides a story. - **New-story count**: the sidebar entry shows an unread count on its right, the page title shows a numeric badge, and matching stories in the list carry a "New" pill plus a blue card outline. The baseline is the last time you left the board; the first install marks the existing backlog as read so it does not start at dozens. - **Category source matrix**: 24 channels are assigned to categories with checkboxes; one channel may belong to several categories. Each channel shows its content leaning (for example "GCORES: Chinese game culture, reviews and developer interviews"). The host fetches the union of channels assigned to **enabled** categories and de-duplicates. - **Enable/disable and delete protection**: every category has an enable switch. A disabled category stops fetching its exclusive sources, disappears from the tab strip, and its exclusive cached stories are hidden from `All` as well. Preset categories cannot be removed (disable/edit only); removing a custom category opens a second-confirmation dialog. - **Bilingual stories**: by default the currently configured DeepSeek Harness model translates English titles/summaries into Chinese and caches them with the feed. You can switch to Baidu Translate (general or LLM), Youdao Translate, or DeepL. Failures fall back to the original; Chinese is shown first with the original as a subtitle. - **Complete settings**: categories and keywords, the category/source matrix, translation toggle and credentials, refresh interval, board size, age window and global blocked words. The save bar lives at the right side of the tab row and only posts the Settings tab's config slice. - **Write-only credentials**: translation secrets/API keys are always returned blank and a `secretSet` sidecar says whether a value is stored; saving a blank field keeps the stored value. - **Low interruption**: no notifications and no sounds. The sidebar entry silently polls every 60 seconds; the host only fetches sources when the cache is stale. - **Bilingual UI**: Chinese / English switches live with Settings β†’ General β†’ Language, no reload. Preset categories and default channels: | Category | Default sources | | --- | --- | | AI | HN AI search, OpenAI News, Google AI Blog, TechCrunch AI, The Verge AI, arXiv cs.AI, MIT Technology Review | | Tech | Hacker News front page, Lobsters, Ars Technica, ITHome, Solidot | | Games | GCORES, YYSTV, GamesIndustry.biz, Rock Paper Shotgun | | Life | SSPAI, Toodaylab, Atlas Obscura | Five more channels are available: Leiphone (AI/industry), 36Kr (tech business), iFanr (consumer electronics/digital life), Indie Games Plus (indie games) and PC Gamer (PC gaming). Each channel has a bilingual content-leaning description in Settings. > All four presets ship with a starter keyword list and pre-checked source channels; both can be edited, cleared or extended at any time. Empty keywords mean the category accepts every story from its assigned sources. ## Install Target runtime: **DeepSeek Harness 0.2.1+** (Desktop `0.2.1-rc.2` and newer, Web 0.2.1+). The package declares `dsh.bundle`; bundle mode is recommended. ### Bundle (recommended) ```sh # Web dsh plugin --profile web add "github:c-ling/dsh-plugin-newsboard#v2.0.0" # or npx @deepseek-ai/dsh plugin --profile web add "github:c-ling/dsh-plugin-newsboard#v2.0.0" # Desktop (quit the app, then use its bundled CLI or install the tag from Plugins) dsh plugin --profile desktop add "github:c-ling/dsh-plugin-newsboard#v2.0.0" ``` `dsh plugin add` writes the package into `dsh.profile.bundles`; the package's `cordis.patch.yml` inserts the loader row. **Do not add the same loader row by hand.** The Desktop profile is managed by the app: `npx @deepseek-ai/dsh ... --profile desktop` is rejected; use the app's bundled CLI or the Plugins page. ### Manual dependency + loader row (pick one, not both) ```sh cd ~/.dsh/profiles/web corepack pnpm add "github:c-ling/dsh-plugin-newsboard#v2.0.0" ``` Then add this to `~/.dsh/profiles/web/cordis.patch.yml`: ```yaml - insert: - id: dsh-plugin-newsboard name: 'dsh-plugin-newsboard' ``` > Do **not** combine bundle mode and a manual loader row; it activates the plugin twice. Restart the host after installing or updating (Web: restart `dsh web`; Desktop: quit and reopen the app), then hard-refresh the page. `client-modules` caches package verdicts per process. Local development can use a `link:` install instead of a GitHub tag: ```sh cd ~/.dsh/profiles/web corepack pnpm add "link:/absolute/path/to/dsh-plugin-newsboard" # then add this plugin's insert row to the profile cordis.patch.yml, or use dsh plugin add "link:..." ``` ## Usage 1. Click `News Board` in the left sidebar; the page opens on `All`. The unread counts in the sidebar and title reflect stories added since you last left. 2. The header search filters within the active section; the refresh button forces a host fetch. 3. Switch to `Settings`: - rename categories, edit keywords, enable/disable categories and add categories; presets cannot be removed and custom removal opens a confirmation dialog; - tick each source's categories in the source matrix; the line under each source explains its leaning; - the save bar is on the right side of the tab row. Saving re-assigns stories immediately and triggers one background refresh. 4. Translation and fetch logs are available from the card headers. 5. Re-entering the page from elsewhere returns to `All`; the tab is not persisted. ## Data and migration - Config: `$DSH_HOME/storages/dsh-plugin-newsboard/config.json` (the entry config `directory` can override the directory). - Feed cache: `feed-cache.json`; logs: `logs.json`. - A legacy `config.json.migrated` is imported into the new `config.json` once at boot; the backup stays in place. - The 0.1.x `settings.yaml` namespace is no longer used. If a config only exists in the settings document, re-enter it on the new page (the archived `config.json.migrated` can recover most fields). ## HTTP surface All endpoints require browser authentication and include `/api` in the path: | Method | Path | Description | | --- | --- | --- | | GET | `/api/dsh-plugin-newsboard/config` | Redacted config + `secretSet` + `revision` + source metadata | | POST | `/api/dsh-plugin-newsboard/config` | Partial patch + `expectedRevision` fence; 409 `STALE_CONFIG` | | GET | `/api/dsh-plugin-newsboard/feed` | Currently visible stories; refreshes in the background when stale | | POST | `/api/dsh-plugin-newsboard/refresh` | Force a refresh now | | GET | `/api/dsh-plugin-newsboard/logs/translation` | Translation API logs | | GET | `/api/dsh-plugin-newsboard/logs/fetch` | Source fetch logs | Config fields: `refreshMinutes`, `maxItems`, `maxAgeHours`, `blockwords`, `categories`, `translate`, `translationProvider`, `translationBaiduMode`, `translationAppId`, `translationSecretKey`, `translationApiKey`. `categories` is an array of `{ id, name, keywords, sources }`. Legacy top-level `keywords` / `sources` are migrated into category keywords and category channels on first boot. POSTing an unknown top-level field returns 400 `UNKNOWN_CONFIG_KEY` β€” if the client hot-reloaded but the host is still an old process, restart the host. ## Development ```sh node --check lib/index.js lib/client.js lib/config.js lib/store.js lib/connection-routes.js node --test python3 -m json.tool package.json git diff --check ``` Source layout: - `lib/index.js` β€” host plugin: source fetching, parsing, dedup/filtering, translation, cache and the `ctx.connection.fetch.register` routes. - `lib/config.js` β€” defaults, normalization, secret redaction and the partial-patch whitelist. - `lib/store.js` β€” atomic `config.json` writes, revision fence and legacy import. - `lib/connection-routes.js` β€” 0.2.1 Fetch route table and error envelopes (dependency-free, testable with a plain `Request`). - `lib/client.js` β€” hand-written factory-CJS bundle, no build step; `inject: ["slots", "locale", "layout"]`, registers `sidebar.panellist` and the keyed `main` page. Verification notes: ```sh # Unauthenticated /api requests stop at the auth gate (401); that does not prove the route exists curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:/api/dsh-plugin-newsboard/config # The old anonymous route must 404 curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:/dsh-plugin-newsboard/config ``` Static UI check: `node tools/render-ui-harness.mjs /tmp/newsboard-ui` emits HTML that reuses the CSS from `lib/client.js` so the list/settings/empty layouts can be inspected in light/dark and at 1440px/1024px. Route existence is asserted by `test/connection-routes.test.mjs` / `test/host.test.mjs` (registry keys start with `/api/...`); check the sidebar entry and page in an authenticated GUI. ## Known limitations - Depends on each source site's RSS/JSON availability. A failed source is shown as "N sources unavailable" in the header and does not affect the others. - The first open with no cache can take up to ~10 seconds for the first fetch; afterwards the cache responds immediately. - Translation is best-effort: a missing model/API or credentials keep the original text. - The plugin deliberately sends no push notifications or sounds; pair it with `dsh-plugin-notify` if you want turn-end reminders. ## License [MIT](LICENSE)