diff --git a/app/(site)/(directories)/cli-apps/categories/[category]/content/ai/about.mdx b/app/(site)/(directories)/cli-apps/categories/[category]/content/ai/about.mdx new file mode 100644 index 000000000..62042d574 --- /dev/null +++ b/app/(site)/(directories)/cli-apps/categories/[category]/content/ai/about.mdx @@ -0,0 +1,12 @@ +## How do you choose an AI tool for the terminal? + +Most of these send your text or code to a model, so start with where that model runs. + +- **Pricing.** Coding agents are often free to install but charge for use, through a model provider's API or a subscription. Check the Pricing filter, then the provider's plans. +- **Local or cloud.** Tools that run models on your machine keep your data local, but they need plenty of RAM and ideally a GPU. +- **Permissions.** Agents can edit files and run commands. Check how each one asks before acting, and start in a project that's under version control so you can undo changes. +- **Platform.** Check the Platform filter, and read the install docs for Windows notes. + +## What should you keep in mind? + +Read what each tool sends to the provider and what it stores. API keys live in config files or environment variables, so keep them out of your shell history and out of any dotfiles you publish. diff --git a/app/(site)/(directories)/cli-apps/categories/[category]/content/ai/intro.mdx b/app/(site)/(directories)/cli-apps/categories/[category]/content/ai/intro.mdx index 56b3174f7..8745c0030 100644 --- a/app/(site)/(directories)/cli-apps/categories/[category]/content/ai/intro.mdx +++ b/app/(site)/(directories)/cli-apps/categories/[category]/content/ai/intro.mdx @@ -1 +1 @@ -Want an AI model or a coding agent in your terminal? This list has coding agents such as Claude Code, Codex CLI, and Aider, tools that run models on your own machine such as Ollama and llama.cpp, and small helpers that send text to a model. Each row shows the type, language, license, and platforms, with the most starred tools first. +Want an AI model or a coding agent in your terminal? This list has coding agents such as Claude Code, Codex CLI, and Aider, tools that run models on your own machine such as Ollama and llama.cpp, and small helpers that send text to a model. Use the filters to narrow the list by app type, platform, language, or license. diff --git a/app/(site)/(directories)/cli-apps/categories/[category]/content/communication/about.mdx b/app/(site)/(directories)/cli-apps/categories/[category]/content/communication/about.mdx new file mode 100644 index 000000000..5ad709f25 --- /dev/null +++ b/app/(site)/(directories)/cli-apps/categories/[category]/content/communication/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a terminal chat or mail client? + +Start with the service. Email, IRC, Mastodon, and feeds each need a different client. + +- **Mail setup.** Some mail clients connect to your server over IMAP and SMTP. Others read mail that a separate sync tool downloads. Check which way each works before you set it up. +- **App type.** Some are full-screen TUI apps you keep open all day. Others are commands you can script. The App type filter separates them, including a Mail client type. +- **Platform.** Most of these are built for Linux and macOS. Check the Platform filter if you're on Windows. +- **Config.** Terminal mail and chat clients reward time spent on configuration. Look for sample configs in the docs before you start. + +## What should you keep in mind? + +Passwords often end up in config files. Use an app password from your provider, and a keyring or a password command where the client supports one. diff --git a/app/(site)/(directories)/cli-apps/categories/[category]/content/databases/about.mdx b/app/(site)/(directories)/cli-apps/categories/[category]/content/databases/about.mdx new file mode 100644 index 000000000..1bbe40fc7 --- /dev/null +++ b/app/(site)/(directories)/cli-apps/categories/[category]/content/databases/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a database CLI? + +Start with the database you use. Most clients only speak one, such as Postgres, MySQL, or SQLite. + +- **Client or engine.** Some tools connect to a database server. Others are the database itself, running on local files. +- **Migrations.** Check whether a migration tool uses plain SQL files or its own format, and whether it supports your database. +- **Language.** Python tools usually install with pip, while Go and Rust tools often ship as a single binary. The Language filter helps if you'd rather not install a runtime. +- **Platform.** Check the Platform filter for your system. + +## What should you keep in mind? + +Connection strings often include passwords. Keep them in a password file or an environment variable, not in your shell history. Run new migrations against a copy of the database before you run them on the real one. diff --git a/app/(site)/(directories)/cli-apps/categories/[category]/content/databases/intro.mdx b/app/(site)/(directories)/cli-apps/categories/[category]/content/databases/intro.mdx index 3a2d58d81..cc00fcbbc 100644 --- a/app/(site)/(directories)/cli-apps/categories/[category]/content/databases/intro.mdx +++ b/app/(site)/(directories)/cli-apps/categories/[category]/content/databases/intro.mdx @@ -1 +1 @@ -Need to query a database or run a migration without leaving the terminal? This list has clients with autocomplete such as pgcli, mycli, and litecli, engines you can run from a prompt such as DuckDB and SQLite, and migration tools such as dbmate, Atlas, and Goose. Each row shows the type, language, license, and platforms. +Need to query a database or run a migration without leaving the terminal? This list has clients with autocomplete such as pgcli, mycli, and litecli, engines you can run from a prompt such as DuckDB and SQLite, and migration tools such as dbmate, Atlas, and Goose. Use the filters to narrow the list by app type, platform, language, or license. diff --git a/app/(site)/(directories)/cli-apps/categories/[category]/content/development/about.mdx b/app/(site)/(directories)/cli-apps/categories/[category]/content/development/about.mdx new file mode 100644 index 000000000..cae340d53 --- /dev/null +++ b/app/(site)/(directories)/cli-apps/categories/[category]/content/development/about.mdx @@ -0,0 +1,13 @@ +## How do you choose a development tool? + +This category is broad, so search for the job first, such as "formatter" or "version manager". Then narrow it down: + +- **Install.** Check the Install filter for the package manager you already use, such as Homebrew, cargo, or npm. +- **Language.** Tools written in Go or Rust usually ship as one binary. Python and Node tools need their runtime installed. +- **Platform.** Check the Platform filter if you work on Windows, or if your team uses different systems. +- **License.** If you'll ship the tool inside your own product, check the License filter. +- **Health.** Build tools end up in every project, so prefer ones the Health filter marks Active. + +## What should you keep in mind? + +Pin tool versions in each project, with a version manager or a lock file, so everyone on the team gets the same results. diff --git a/app/(site)/(directories)/cli-apps/categories/[category]/content/development/intro.mdx b/app/(site)/(directories)/cli-apps/categories/[category]/content/development/intro.mdx index f5a11ae99..58725181f 100644 --- a/app/(site)/(directories)/cli-apps/categories/[category]/content/development/intro.mdx +++ b/app/(site)/(directories)/cli-apps/categories/[category]/content/development/intro.mdx @@ -1 +1 @@ -Building software and want better tools in your terminal? This is the largest category in the directory. It covers runtimes and toolchains such as Go, Deno, and Bun, editors such as Neovim and Emacs, package managers such as uv, and coding agents. Rows are sorted by GitHub stars, so the most starred tools come first. +Building software and want better tools in your terminal? This is the largest category in the directory. It covers runtimes and toolchains such as Go, Deno, and Bun, editors such as Neovim and Emacs, package managers such as uv, and coding agents. diff --git a/app/(site)/(directories)/cli-apps/categories/[category]/content/devops/about.mdx b/app/(site)/(directories)/cli-apps/categories/[category]/content/devops/about.mdx new file mode 100644 index 000000000..070c82a58 --- /dev/null +++ b/app/(site)/(directories)/cli-apps/categories/[category]/content/devops/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a DevOps CLI? + +Start with the platforms you run. Many tools here are the official CLI for one service, so that choice is already made for you. + +- **Pricing.** Some tools are tied to a paid service. Check the Pricing filter and the service's own plans. +- **Source model.** A few tools here are source available or closed source. Check the Source model filter if that matters to you. +- **State and credentials.** Infrastructure tools keep state files and keys. Check where each tool stores them, and keep both out of Git. +- **Platform.** Check the Platform filter for your CI runners as well as your own computer. + +## What should you keep in mind? + +These tools can change production with one command. Use a read-only or plan mode first where the tool has one, and give CI tokens only the permissions they need. diff --git a/app/(site)/(directories)/cli-apps/categories/[category]/content/devops/intro.mdx b/app/(site)/(directories)/cli-apps/categories/[category]/content/devops/intro.mdx index 2654c1348..8873e5928 100644 --- a/app/(site)/(directories)/cli-apps/categories/[category]/content/devops/intro.mdx +++ b/app/(site)/(directories)/cli-apps/categories/[category]/content/devops/intro.mdx @@ -1 +1 @@ -Deploying an app or looking after a server from the terminal? This list covers infrastructure tools such as Terraform and Ansible, container and Kubernetes tools such as lazydocker, dive, and K9s, and the official CLIs for GitHub, GitLab, Fly.io, Railway, Supabase, and Stripe. Each row shows the type, language, license, and platforms. +Deploying an app or looking after a server from the terminal? This list covers infrastructure tools such as Terraform and Ansible, container and Kubernetes tools such as lazydocker, dive, and K9s, and the official CLIs for GitHub, GitLab, Fly.io, Railway, Supabase, and Stripe. Use the filters to narrow the list by app type, platform, language, or license. diff --git a/app/(site)/(directories)/cli-apps/categories/[category]/content/files/about.mdx b/app/(site)/(directories)/cli-apps/categories/[category]/content/files/about.mdx new file mode 100644 index 000000000..54ff772c7 --- /dev/null +++ b/app/(site)/(directories)/cli-apps/categories/[category]/content/files/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a file tool? + +Name the job first: search, move, sync, back up, or browse. + +- **Search defaults.** Some newer search tools skip hidden files and anything in `.gitignore` by default. Check that this matches what you want to find. +- **Sync and backup.** Check which remotes it supports, such as S3 or SFTP, and whether backups are encrypted and keep older versions. +- **App type.** File managers are full-screen apps you move around in, while finders are commands you can pipe into others. The App type filter separates them. +- **Platform.** Check the Platform filter, especially on Windows. + +## What should you keep in mind? + +Commands that delete, move, or remove duplicate files can't be undone. Run a dry run first when the tool has one, and keep a backup before a big cleanup. diff --git a/app/(site)/(directories)/cli-apps/categories/[category]/content/games-fun/about.mdx b/app/(site)/(directories)/cli-apps/categories/[category]/content/games-fun/about.mdx new file mode 100644 index 000000000..2680c9972 --- /dev/null +++ b/app/(site)/(directories)/cli-apps/categories/[category]/content/games-fun/about.mdx @@ -0,0 +1,12 @@ +## How do you pick a terminal game or toy? + +These are small and quick to try, so the checks are simple. + +- **Platform.** Most are built for Linux and macOS. Check the Platform filter if you're on Windows, or run them in WSL. +- **Install.** Check the Install filter for a package manager you have. +- **Your terminal.** Some games need color, Unicode, or a minimum window size. If one looks wrong, try a bigger window, another font, or another terminal. +- **Controls.** Check the docs for the keys. Many use the arrow keys or Vim-style keys. + +## Why play in a terminal? + +Terminal games start instantly, use almost no resources, and work over SSH on any server. Some are long-running classics with deep rules, so expect to spend time learning them. diff --git a/app/(site)/(directories)/cli-apps/categories/[category]/content/index.ts b/app/(site)/(directories)/cli-apps/categories/[category]/content/index.ts index 05b38951b..dff102265 100644 --- a/app/(site)/(directories)/cli-apps/categories/[category]/content/index.ts +++ b/app/(site)/(directories)/cli-apps/categories/[category]/content/index.ts @@ -13,7 +13,22 @@ import Intro10 from "./communication/intro.mdx"; import Intro11 from "./games-fun/intro.mdx"; import Intro12 from "./terminal-ui/intro.mdx"; import Intro13 from "./ai/intro.mdx"; +import About0 from "./development/about.mdx"; +import About1 from "./devops/about.mdx"; +import About2 from "./files/about.mdx"; +import About3 from "./text/about.mdx"; +import About4 from "./networking/about.mdx"; +import About5 from "./security/about.mdx"; +import About6 from "./monitoring/about.mdx"; +import About7 from "./databases/about.mdx"; +import About8 from "./media-graphics/about.mdx"; +import About9 from "./productivity/about.mdx"; +import About10 from "./communication/about.mdx"; +import About11 from "./games-fun/about.mdx"; +import About12 from "./terminal-ui/about.mdx"; +import About13 from "./ai/about.mdx"; +// One intro (above the apps) and one about section (below the apps) per category. export const categoryIntros: Record = { "development": Intro0, "devops": Intro1, @@ -30,3 +45,20 @@ export const categoryIntros: Record = { "terminal-ui": Intro12, "ai": Intro13, }; + +export const categoryAbouts: Record = { + "development": About0, + "devops": About1, + "files": About2, + "text": About3, + "networking": About4, + "security": About5, + "monitoring": About6, + "databases": About7, + "media-graphics": About8, + "productivity": About9, + "communication": About10, + "games-fun": About11, + "terminal-ui": About12, + "ai": About13, +}; diff --git a/app/(site)/(directories)/cli-apps/categories/[category]/content/media-graphics/about.mdx b/app/(site)/(directories)/cli-apps/categories/[category]/content/media-graphics/about.mdx new file mode 100644 index 000000000..d223c6df5 --- /dev/null +++ b/app/(site)/(directories)/cli-apps/categories/[category]/content/media-graphics/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a media tool? + +Name the job first: convert, download, play, or view. + +- **Formats and codecs.** Check that it handles the formats you need. Some builds leave out codecs for license reasons, so the version from your package manager may differ from the official one. +- **Downloaders** break when websites change. Prefer ones the Health filter marks Active, and update them often. +- **Images in the terminal** look best in a terminal that supports an image protocol, such as Kitty's or Sixel. Otherwise they fall back to colored blocks of text. +- **Platform.** Check the Platform filter for your system. + +## What should you keep in mind? + +Media commands can overwrite files without asking. Write the output to a new file until you trust the command. Only download media you have the right to keep. diff --git a/app/(site)/(directories)/cli-apps/categories/[category]/content/monitoring/about.mdx b/app/(site)/(directories)/cli-apps/categories/[category]/content/monitoring/about.mdx new file mode 100644 index 000000000..78fa672ff --- /dev/null +++ b/app/(site)/(directories)/cli-apps/categories/[category]/content/monitoring/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a monitoring CLI? + +Decide what you're watching: processes, disk space, network traffic, or logs. + +- **Live or one-off.** Process monitors refresh on screen as things change. Disk usage tools take one snapshot and exit. The App type filter separates them. +- **Permissions.** Tools that watch network traffic often need root or extra permissions to see it. +- **Platform.** Monitoring depends on the operating system, so check the Platform filter. Some features only work on Linux. +- **Remote servers.** Check that the tool runs over SSH with nothing extra to install on your side. + +## What should you keep in mind? + +These tools show what's happening right now. For history and alerts, you need a monitoring server. Search the [self-host apps directory](/self-host-apps) for one you can run yourself. diff --git a/app/(site)/(directories)/cli-apps/categories/[category]/content/networking/about.mdx b/app/(site)/(directories)/cli-apps/categories/[category]/content/networking/about.mdx new file mode 100644 index 000000000..5cdf94c86 --- /dev/null +++ b/app/(site)/(directories)/cli-apps/categories/[category]/content/networking/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a networking tool? + +Name the problem first: an API call, a DNS answer, a slow route, or a service you need to reach. + +- **HTTP clients.** Some are for quick requests while you explore. Others run request files you can keep in Git as tests. Pick by which you do more. +- **DNS tools.** Check the output format and whether it supports DNS over HTTPS, if you need it. +- **Permissions.** Packet capture and some tracing tools need root. +- **Platform.** Check the Platform filter. Some network tools only run on Linux and macOS. + +## What should you keep in mind? + +Only scan or capture traffic on networks you own or have permission to test. A tunnel exposes a local port to the internet, so close it when you're done. diff --git a/app/(site)/(directories)/cli-apps/categories/[category]/content/productivity/about.mdx b/app/(site)/(directories)/cli-apps/categories/[category]/content/productivity/about.mdx new file mode 100644 index 000000000..ccd850977 --- /dev/null +++ b/app/(site)/(directories)/cli-apps/categories/[category]/content/productivity/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a terminal productivity tool? + +Start with the thing you do most in the terminal, then pick a tool for that. + +- **Shell support.** Jumpers and finders hook into your shell. Check that they support yours, such as Bash, Zsh, or fish. +- **Config.** Session and dotfile managers live on their config. Check that you can keep it in Git and share it between machines. +- **Platform.** Check the Platform filter if you move between macOS, Linux, and Windows. +- **Health.** You'll use these every day for years, so prefer ones the Health filter marks Active. + +## What should you keep in mind? + +Add tools one at a time, and get used to each before the next. Dotfile managers can end up holding secrets, so check how they handle encryption before you push your dotfiles to a public repository. diff --git a/app/(site)/(directories)/cli-apps/categories/[category]/content/security/about.mdx b/app/(site)/(directories)/cli-apps/categories/[category]/content/security/about.mdx new file mode 100644 index 000000000..75e16e207 --- /dev/null +++ b/app/(site)/(directories)/cli-apps/categories/[category]/content/security/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a security CLI? + +Name the job first: scan code, scan systems, encrypt files, or manage certificates. + +- **Scanners.** Check what they detect and how noisy the results are. Running them in CI catches problems before code is merged. +- **Encryption.** Check the key formats and how you share keys with others. A tool with fewer options is easier to use safely. +- **Source model.** For encryption, open source lets others check the code. The Source model filter shows this. +- **Health.** Security tools need regular updates. Prefer ones the Health filter marks Active. + +## What should you keep in mind? + +Only scan systems you own or have permission to test. Keep private keys off shared machines, and back them up somewhere safe and offline. diff --git a/app/(site)/(directories)/cli-apps/categories/[category]/content/terminal-ui/about.mdx b/app/(site)/(directories)/cli-apps/categories/[category]/content/terminal-ui/about.mdx new file mode 100644 index 000000000..c94962bd0 --- /dev/null +++ b/app/(site)/(directories)/cli-apps/categories/[category]/content/terminal-ui/about.mdx @@ -0,0 +1,14 @@ +## How do you choose a terminal, shell, or prompt? + +These pieces stack. A terminal emulator runs a shell, the shell shows a prompt, and TUI apps run inside it all. Pick each layer on its own. + +- **Terminal emulators.** Check for splits, tabs, and image support. Some include cloud or AI features, so read their privacy notes. +- **Shells.** Bash and Zsh run most scripts you'll find online. Other shells trade that compatibility for nicer defaults. +- **Prompts and themes.** Many need a Nerd Font, a font with extra icons, to look right. +- **Platform.** Check the Platform filter. Some emulators only run on macOS or Linux. + +The App type filter separates terminal emulators, shells, prompts, multiplexers, and TUI apps. + +## What should you keep in mind? + +Keep your shell and terminal config in a dotfiles repository, so a new machine feels like home in minutes. diff --git a/app/(site)/(directories)/cli-apps/categories/[category]/content/text/about.mdx b/app/(site)/(directories)/cli-apps/categories/[category]/content/text/about.mdx new file mode 100644 index 000000000..acf5f0c0f --- /dev/null +++ b/app/(site)/(directories)/cli-apps/categories/[category]/content/text/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a text tool? + +Name the job first: edit, search, transform, compare, or convert. + +- **Editors.** Modal editors like Vim take time to learn, but vi or Vim is on almost every server. Others work like a desktop editor from the start. +- **Structured data.** For JSON, YAML, or CSV, use a tool built for that format instead of search and replace. +- **Pipes.** Check that the tool reads from standard input and writes plain output, so it fits into scripts. +- **Platform and install.** Check the Platform and Install filters for your system. + +## What should you keep in mind? + +Commands that edit files in place can't be undone. Keep files in Git, or try the command on a copy first. diff --git a/app/(site)/(directories)/cli-apps/categories/[category]/page.tsx b/app/(site)/(directories)/cli-apps/categories/[category]/page.tsx index d85f65786..074a16e6f 100644 --- a/app/(site)/(directories)/cli-apps/categories/[category]/page.tsx +++ b/app/(site)/(directories)/cli-apps/categories/[category]/page.tsx @@ -1,29 +1,80 @@ import { notFound } from "next/navigation"; import { getRequiredDirectoryPage } from "@/lib/directory-pages"; import { getAppCategoryLinks } from "@/lib/app-categories"; -import { appCategoryStaticParams, selectCategoryApps, MIN_CATEGORY_APPS } from "@/lib/app-category-model"; -import { getTerminalApps, terminalAppTypeLabels } from "@/lib/terminal-apps"; -import { AppCategoryPage } from "@/app/ui/directories/app-category-page"; -import { categoryIntros } from "./content"; +import { appCategoryStaticParams, MIN_CATEGORY_APPS } from "@/lib/app-category-model"; +import { getBreadcrumbs } from "@/lib/breadcrumbs"; +import { getTerminalApps } from "@/lib/terminal-apps"; +import { AppCategoryLinks } from "@/app/ui/directories/app-category-links"; +import { DirectoryPageTitle } from "@/app/ui/directories/directory-page-content"; +import { DirectoryQueryProvider } from "@/app/ui/directories/directory-query"; +import { HiddenDetails } from "@/app/ui/directories/hidden-details"; +import { AppShell, SidebarLabel } from "@/app/ui/site/app-shell"; +import { Doodle } from "@/app/ui/site/doodle"; +import { EntryHero } from "@/app/ui/site/entry-hero"; +import { PageCanvas } from "@/app/ui/site/page-canvas"; +import { PageHeader } from "@/app/ui/site/page-header"; +import { PageMeta } from "@/app/ui/site/page-meta"; +import { proseClassName, proseSectionFlowClassName } from "@/app/ui/site/prose"; +import { siteDoodleStarArt } from "../../../../art/site--doodle-star"; +import { siteSpotCliAppsArt } from "../../../../art/site--spot-cli-apps"; +import { CliAppDetailsBody } from "../../cli-app-details"; +import { cliAppFilterOptions } from "../../cli-app-facets"; +import { cliAppPageSize } from "../../cli-app-options"; +import { CliAppFilters, CliAppResults } from "../../cli-app-view"; +import { filterApps, sortApps } from "../../filters"; +import { categoryAbouts, categoryIntros } from "./content"; export const revalidate = false; export async function generateStaticParams() { return appCategoryStaticParams(await getAppCategoryLinks("cli-apps")); } +// The static page is the category's default view: most popular first, page 1. +// Filters, search, sort, and later pages run in the browser on the cached CLI +// index, always limited to this category, and the URL stays on this page. export default async function CategoryPage({ params }: { params: Promise<{ category: string }> }) { const { category } = await params; + const route = `/cli-apps/categories/${category}`; const directoryPage = await getRequiredDirectoryPage(`cli-apps/categories/${category}`); const categories = await getAppCategoryLinks("cli-apps"); - if (!categories.some(entry => entry.slug === category)) notFound(); + const current = categories.find(entry => entry.slug === category); + if (!current) notFound(); const Intro = categoryIntros[category]; if (!Intro) throw new Error(`Missing CLI category intro: ${category}`); - const apps = selectCategoryApps(await getTerminalApps(), category, app => app.categories, app => app.stars ?? 0); + const About = categoryAbouts[category]; + if (!About) throw new Error(`Missing CLI category about section: ${category}`); + const apps = sortApps(filterApps(await getTerminalApps(), { category }), "popular"); if (apps.length < MIN_CATEGORY_APPS) notFound(); - return } categories={categories} apps={apps.map(app => ({ - name: app.name, summary: app.summary, href: app.repository_url || app.website_url, - facts: [terminalAppTypeLabels.get(app.primary_type) ?? app.primary_type, app.language || "Language unknown", - app.license || "License unknown", app.platforms.length ? app.platforms.join(", ") : "Platforms unknown"].join(" · "), - }))} />; + const defaultApps = apps.slice(0, cliAppPageSize); + const options = cliAppFilterOptions(apps); + const categoryLabels = Object.fromEntries(options.category.map((option) => [option.value, option.label])); + + return ( + + {current.name}} options={options} route={route} />} + sidebarLabel={`${current.name} app filters`} + > + + +

+ + +
+
+ + } route={route} /> + ({ slug: app.slug, name: app.name, body: }))} /> + +
+
+ +
+
+
+
+
+ ); } diff --git a/app/(site)/(directories)/cli-apps/cli-app-facets.ts b/app/(site)/(directories)/cli-apps/cli-app-facets.ts new file mode 100644 index 000000000..317a80f79 --- /dev/null +++ b/app/(site)/(directories)/cli-apps/cli-app-facets.ts @@ -0,0 +1,43 @@ +import type { DirectoryFilterOption as FilterOption } from "@/app/ui/directories/directory-filters"; +import { terminalAppCategories, terminalAppTypeLabels, type TerminalApp } from "@/lib/terminal-apps"; +import { installLabels, labelForValue, platformOptions } from "./cli-app-options"; +import type { CliAppFilterOptions } from "./cli-app-view"; + +const typeLabels = new Map(terminalAppTypeLabels); + +function countOptions(counts: Map, label: (value: string) => string): FilterOption[] { + return [...counts.entries()] + .map(([value, count]) => ({ count, label: label(value), value })) + .sort((a, b) => a.label.localeCompare(b.label)); +} + +function count(apps: TerminalApp[], values: (app: TerminalApp) => string[]) { + const counts = new Map(); + for (const app of apps) for (const value of values(app)) counts.set(value, (counts.get(value) ?? 0) + 1); + return counts; +} + +/** Sidebar options and counts for the apps in view: the whole directory, or one category. */ +export function cliAppFilterOptions(apps: TerminalApp[]): CliAppFilterOptions { + const categoryCounts = count(apps, (app) => app.categories); + const lower = (value: string | null) => (value ?? "unknown").toLowerCase(); + const unknownLabel = (value: string) => value === "unknown" ? "Unknown" : value; + return { + category: terminalAppCategories + .filter((category) => categoryCounts.has(category.slug)) + .map((category) => ({ + count: categoryCounts.get(category.slug), + iconKey: category.icon_key, + label: category.name, + value: category.slug, + })), + install: countOptions(count(apps, (app) => app.install_methods), (value) => installLabels.get(value) ?? value), + language: countOptions(count(apps, (app) => [lower(app.language)]), unknownLabel), + license: countOptions(count(apps, (app) => [lower(app.license)]), unknownLabel), + platform: platformOptions.map((option) => ({ + ...option, + count: apps.filter((app) => app.platforms.includes(option.value)).length, + })), + type: countOptions(count(apps, (app) => [app.primary_type, ...app.additional_types]), (value) => labelForValue(typeLabels, value)), + }; +} diff --git a/app/(site)/(directories)/cli-apps/cli-app-view.tsx b/app/(site)/(directories)/cli-apps/cli-app-view.tsx index 62117bc89..5021f4537 100644 --- a/app/(site)/(directories)/cli-apps/cli-app-view.tsx +++ b/app/(site)/(directories)/cli-apps/cli-app-view.tsx @@ -31,7 +31,6 @@ import { } from "./cli-app-options"; import { filterApps, getKnownValue, getPage, getString, getValues, sortApps, sortOptions, sortValues } from "./filters"; -const route = "/cli-apps"; const paramKeys = ["q", "sort", "page", "category", "type", "platform", "install", "language", "license", "pricing", "source", "maintenance"] as const; export type CliAppFilterOptions = { @@ -43,8 +42,22 @@ export type CliAppFilterOptions = { type: FilterOption[]; }; -export function CliAppFilters({ heading, options }: { heading: ReactNode; options: CliAppFilterOptions }) { - const { formKey, params } = useDirectoryQuery(); +/** A category page drops any `category` in its URL: its category is fixed. */ +function viewParams(params: DirectoryParams, category: string | undefined) { + if (!category || !("category" in params)) return params; + return Object.fromEntries(Object.entries(params).filter(([key]) => key !== "category")); +} + +// `route` is the page path: the hub, or a category page. A category page passes +// its `category`, hides the Category group, and keeps every link on its own path. +export function CliAppFilters({ category, heading, options, route }: { + category?: string; + heading: ReactNode; + options: CliAppFilterOptions; + route: string; +}) { + const { formKey, params: urlParams } = useDirectoryQuery(); + const params = viewParams(urlParams, category); return (
{heading}
@@ -58,7 +71,7 @@ export function CliAppFilters({ heading, options }: { heading: ReactNode; option - + {category ? null : } @@ -88,7 +101,7 @@ const groupNames = new Map([ ["license", "License"], ]); -function ActiveFilters({ categoryLabels, params }: { categoryLabels: Record; params: DirectoryParams }) { +function ActiveFilters({ categoryLabels, params, route }: { categoryLabels: Record; params: DirectoryParams; route: string }) { const groupLabels = new Map>([ ["type", new Map(terminalAppTypeLabels)], ["category", new Map(Object.entries(categoryLabels))], @@ -132,20 +145,24 @@ function ActiveFilters({ categoryLabels, params }: { categoryLabels: Record; emptyVisual: ReactNode; + route: string; }) { - const { params } = useDirectoryQuery(); + const { params: urlParams } = useDirectoryQuery(); + const params = viewParams(urlParams, category); const filtering = hasDirectoryParams(params, paramKeys); const { failed, records } = useDirectoryIndex({ url: "/api/cli-apps/index" }, filtering); if (filtering && !records) { return <> - + {failed ? show all apps} state="error" title="Filters could not be loaded">

Reload the page to try again.

: } @@ -153,7 +170,9 @@ export function CliAppResults({ firstPage, total, categoryLabels, emptyVisual }: } const sort = getKnownValue(params, "sort", sortValues) || "popular"; - const filteredApps = filtering && records ? sortApps(filterApps(records, params), sort) : null; + const filteredApps = filtering && records + ? sortApps(filterApps(records, category ? { ...params, category } : params), sort) + : null; const count = filteredApps ? filteredApps.length : total; const totalPages = Math.max(1, Math.ceil(count / cliAppPageSize)); const currentPage = Math.min(getPage(params), totalPages); @@ -162,7 +181,7 @@ export function CliAppResults({ firstPage, total, categoryLabels, emptyVisual }: return ( <> - + {paginatedApps.length > 0 ? ( <> diff --git a/app/(site)/(directories)/cli-apps/page.tsx b/app/(site)/(directories)/cli-apps/page.tsx index f3ff51fa6..6fe5d3850 100644 --- a/app/(site)/(directories)/cli-apps/page.tsx +++ b/app/(site)/(directories)/cli-apps/page.tsx @@ -7,13 +7,8 @@ import { DirectoryPageTitle } from "@/app/ui/directories/directory-page-content" import { PageMeta } from "@/app/ui/site/page-meta"; import { PageCanvas } from "@/app/ui/site/page-canvas"; import { getRequiredPublishedPage } from "@/lib/page-registry"; -import { - terminalAppCategories, - terminalAppTypeLabels, -} from "@/lib/terminal-apps"; import { getTerminalApps } from "@/lib/terminal-apps"; import { AppShell, SidebarLabel } from "@/app/ui/site/app-shell"; -import type { DirectoryFilterOption as FilterOption } from "../../../ui/directories/directory-filters"; import { DirectoryQueryProvider } from "../../../ui/directories/directory-query"; import { Doodle } from "@/app/ui/site/doodle"; import { siteDoodleStarArt } from "../../art/site--doodle-star"; @@ -23,7 +18,8 @@ import { EntryHero } from "@/app/ui/site/entry-hero"; import { siteSpotCliAppsArt } from "../../art/site--spot-cli-apps"; import { CliAppDetailsBody } from "./cli-app-details"; import { HiddenDetails } from "@/app/ui/directories/hidden-details"; -import { cliAppPageSize, installLabels, labelForValue, platformOptions } from "./cli-app-options"; +import { cliAppFilterOptions } from "./cli-app-facets"; +import { cliAppPageSize } from "./cli-app-options"; import { CliAppFilters, CliAppResults } from "./cli-app-view"; import { sortApps } from "./filters"; import { proseClassName, proseSectionFlowClassName } from "@/app/ui/site/prose"; @@ -33,7 +29,7 @@ import Intro from "./content/intro.mdx"; import SourceModel from "./content/source-model.mdx"; import Types from "./content/types.mdx"; -const typeLabels = new Map(terminalAppTypeLabels); +const route = "/cli-apps"; // Static page: the default view (most popular, page 1). Filters run in the browser. export default async function CliAppsPage() { @@ -44,100 +40,21 @@ export default async function CliAppsPage() { ]); const defaultApps = sortApps(apps, "popular").slice(0, cliAppPageSize); - const categoryCounts = new Map(); - for (const app of apps) { - for (const category of app.categories) { - categoryCounts.set(category, (categoryCounts.get(category) ?? 0) + 1); - } - } - const categoryOptions = terminalAppCategories - .filter((category) => categoryCounts.has(category.slug)) - .map((category) => ({ - count: categoryCounts.get(category.slug), - iconKey: category.icon_key, - label: category.name, - value: category.slug, - })); - const categoryLabels = Object.fromEntries(categoryOptions.map((option) => [option.value, option.label])); - - const typeCounts = new Map(); - for (const app of apps) { - typeCounts.set(app.primary_type, (typeCounts.get(app.primary_type) ?? 0) + 1); - for (const type of app.additional_types) { - typeCounts.set(type, (typeCounts.get(type) ?? 0) + 1); - } - } - const typeFilterOptions = [...typeCounts.entries()] - .map(([value, count]) => ({ - count, - label: labelForValue(typeLabels, value), - value, - })) - .sort((a, b) => a.label.localeCompare(b.label)); - - const installCounts = new Map(); - for (const app of apps) { - for (const method of app.install_methods) { - installCounts.set(method, (installCounts.get(method) ?? 0) + 1); - } - } - const installOptions = [...installCounts.entries()] - .map(([value, count]) => ({ - count, - label: installLabels.get(value) ?? value, - value, - })) - .sort((a, b) => a.label.localeCompare(b.label)); - - const platformFilterOptions = platformOptions.map((option) => ({ - ...option, - count: apps.filter((app) => app.platforms.includes(option.value)).length, - })); - - const languageCounts = new Map(); - for (const app of apps) { - const key = (app.language ?? "unknown").toLowerCase(); - languageCounts.set(key, (languageCounts.get(key) ?? 0) + 1); - } - const languageOptions: FilterOption[] = [...languageCounts.entries()] - .map(([value, count]) => ({ - count, - label: value === "unknown" ? "Unknown" : value, - value, - })) - .sort((a, b) => a.label.localeCompare(b.label)); - - const licenseCounts = new Map(); - for (const app of apps) { - const key = (app.license ?? "unknown").toLowerCase(); - licenseCounts.set(key, (licenseCounts.get(key) ?? 0) + 1); - } - const licenseOptions: FilterOption[] = [...licenseCounts.entries()] - .map(([value, count]) => ({ - count, - label: value === "unknown" ? "Unknown" : value, - value, - })) - .sort((a, b) => a.label.localeCompare(b.label)); + const options = cliAppFilterOptions(apps); + const categoryLabels = Object.fromEntries(options.category.map((option) => [option.value, option.label])); const sidebar = ( {registryPage.label}} - options={{ - category: categoryOptions, - install: installOptions, - language: languageOptions, - license: licenseOptions, - platform: platformFilterOptions, - type: typeFilterOptions, - }} + options={options} + route={route} /> ); const categoryLinks = await getAppCategoryLinks("cli-apps"); return ( - + - } /> + } route={route} /> ({ slug: app.slug, name: app.name, body: }))} />
diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/ai-ml/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/ai-ml/about.mdx new file mode 100644 index 000000000..ea22771f0 --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/ai-ml/about.mdx @@ -0,0 +1,12 @@ +## How do you pick a self-hosted AI app? + +Start with your hardware. Most model runners lean on a GPU, and a large model needs far more memory than the app itself. The RAM on each card is for the app, not the model you load, so check the model's size too. + +- **Runner or front end.** A chat front end needs a model runner or an API key behind it. Pick the runner first, then a front end that talks to it. +- **License.** Some AI apps are source available or limit commercial use. Use the Source model filter, and check each model's license on its own, since whoever trained it sets that. +- **Health.** AI projects move fast and stall fast. The Health filter shows which still get commits. +- **Docker.** Using a GPU inside Docker takes extra setup, such as the NVIDIA Container Toolkit on Linux. Read the install docs before you choose Docker. + +## What should you plan for when you run one? + +Models are big files, often several gigabytes each, so plan disk space for them. Before you open the app to the internet, check that it has its own login. If it doesn't, put one in front of it. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/ai-ml/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/ai-ml/intro.mdx index 30b023d47..36e955209 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/ai-ml/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/ai-ml/intro.mdx @@ -1 +1 @@ -Want to run AI models and agents on your own hardware instead of paying for a cloud service? This list has model runners such as Ollama, chat front ends such as Open WebUI, image tools such as ComfyUI, and agent and workflow builders such as Dify and Langflow. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Want to run AI models and agents on your own hardware instead of paying for a cloud service? This list has model runners such as Ollama, chat front ends such as Open WebUI, image tools such as ComfyUI, and agent and workflow builders such as Dify and Langflow. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/automation/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/automation/about.mdx new file mode 100644 index 000000000..35cad0a3e --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/automation/about.mdx @@ -0,0 +1,12 @@ +## How do you choose an automation app? + +Start with what kicks off your work and which services it needs to reach. + +- **Integrations.** A workflow builder is only as useful as the services it connects to. Check its integration list for the apps you use before you install. +- **License.** Some popular workflow tools are source available, which can limit running them for clients. Use the Source model filter, and read [software licenses for self-hosting](/software-licenses-for-self-hosting) if you plan to sell a service built on one. +- **Workers and database.** Bigger builders run background workers, a queue, and a database. The Needs database signal shows the ones we've confirmed need one. +- **Health.** Scrapers and browser bots break when websites change. Prefer ones the Health filter marks Active. + +## What should you expect when it runs? + +Automation runs all day, so it needs a server that stays on. It also stores logins and API keys for every service it touches. Back it up, and keep it off the open internet unless incoming webhooks need a public address. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/automation/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/automation/intro.mdx index 4b2013db7..4aad03c75 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/automation/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/automation/intro.mdx @@ -1 +1 @@ -Tired of paying per task for Zapier or doing the same steps by hand? This list has workflow builders such as n8n and Dify, scrapers such as Firecrawl, browser automation such as browserless, and small bots and job runners that act for you. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Tired of paying per task for Zapier or doing the same steps by hand? This list has workflow builders such as n8n and Dify, scrapers such as Firecrawl, browser automation such as browserless, and small bots and job runners that act for you. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/bookmarks-read-it-later/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/bookmarks-read-it-later/about.mdx new file mode 100644 index 000000000..62b95602d --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/bookmarks-read-it-later/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a bookmark or reading app? + +Decide first what you want to keep: links, articles to read later, feeds, or full copies of pages. Each is a different job, and only some apps do two. + +- **Saving from anywhere.** You'll save most links from a browser or a phone. Check the app's links for a browser extension and a phone app or share option. +- **Import.** If you're moving from another service, look for import of its export file. Feed readers use OPML files for this. +- **Storage.** Archivers that keep full pages or screenshots grow fast. Bookmark-only apps stay small. The Storage filter shows the difference. +- **RAM.** Some bookmark apps add AI tagging or run a headless browser to grab pages, which takes more memory. Use the RAM filter on a small server. + +## What should you back up? + +The database holds your links, tags, and read status. Back it up with any saved page archive. A feed reader checks feeds on a timer, so it needs to stay running to catch every post. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/bookmarks-read-it-later/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/bookmarks-read-it-later/intro.mdx index 34c40f5a3..5a1449ff7 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/bookmarks-read-it-later/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/bookmarks-read-it-later/intro.mdx @@ -1 +1 @@ -Want your saved links and feeds on your own server, not in Pocket or Feedly? This list has bookmark managers such as Karakeep, Linkwarden, and linkding, read-it-later apps such as Wallabag, RSS readers such as FreshRSS and Miniflux, and web archivers such as ArchiveBox. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Want your saved links and feeds on your own server, not in Pocket or Feedly? This list has bookmark managers such as Karakeep, Linkwarden, and linkding, read-it-later apps such as Wallabag, RSS readers such as FreshRSS and Miniflux, and web archivers such as ArchiveBox. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/business-commerce/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/business-commerce/about.mdx new file mode 100644 index 000000000..7a1d4a472 --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/business-commerce/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a business app? + +Pick by the job you need done first, such as selling, invoicing, support, or customer records. Suites that try to do everything take the longest to set up. + +- **License and editions.** Some ERP and CRM projects sell a paid edition next to a free one. Check the License and Source model filters, then read what the free edition leaves out. +- **Health.** You'll run this for years, so the Health filter matters more here than for most apps. +- **Resources.** ERP suites usually want more RAM than a small VPS has. Use the RAM filter before you commit. +- **Payments and tax.** For a shop, check that the app supports your payment provider and your country's tax rules. That's on the app's own site, not the card. + +## What should you plan for before you run one? + +Orders, invoices, and customer details are data you can't lose. Plan backups of the database and uploaded files from day one, and keep the app updated. Customer data can also bring privacy duties in the places you sell. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/business-commerce/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/business-commerce/intro.mdx index f260f6e35..6b45dbf81 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/business-commerce/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/business-commerce/intro.mdx @@ -1 +1 @@ -Running a business and want your customer and sales data on your own server? This list has CRMs such as Twenty, ERP suites such as Odoo and ERPNext, online shops such as Saleor, Medusa, and Bagisto, and support desks such as Chatwoot. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Running a business and want your customer and sales data on your own server? This list has CRMs such as Twenty, ERP suites such as Odoo and ERPNext, online shops such as Saleor, Medusa, and Bagisto, and support desks such as Chatwoot. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/calendars-contacts/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/calendars-contacts/about.mdx new file mode 100644 index 000000000..881567392 --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/calendars-contacts/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a calendar or contacts server? + +For most people, the deciding question is whether the phone and desktop apps you already use can connect to it. + +- **CalDAV and CardDAV.** These open standards sync calendars and contacts. iPhone, macOS, and Thunderbird support them directly. Android usually needs an extra sync app. If you want both calendars and contacts, check the server supports both. +- **Size.** A simple sync server needs very little. Use the RAM filter to find light options for a Raspberry Pi or a NAS. +- **Sharing.** If a family or team shares calendars, check how shared calendars and permissions work. +- **Booking pages and polls** are separate tools. They usually connect to your calendar rather than replace it. + +## What should you back up? + +Your calendars and address book live in a database or in plain files on the server. Back them up, and export everything once before you switch servers. To sync while you're away from home, you'll need HTTPS and a way in, such as a VPN or a reverse proxy. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/calendars-contacts/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/calendars-contacts/intro.mdx index 00bcc08ae..915cf8882 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/calendars-contacts/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/calendars-contacts/intro.mdx @@ -1 +1 @@ -Want your calendars and contacts off Google or iCloud? This list has CalDAV and CardDAV servers such as Radicale, Baïkal, and SabreDAV, booking pages such as Easy!Appointments, group schedulers such as Rallly, and Monica for keeping track of people. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Want your calendars and contacts off Google or iCloud? This list has CalDAV and CardDAV servers such as Radicale, Baïkal, and SabreDAV, booking pages such as Easy!Appointments, group schedulers such as Rallly, and Monica for keeping track of people. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/communication/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/communication/about.mdx new file mode 100644 index 000000000..eaaede6c7 --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/communication/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a chat or messaging server? + +Start with who will use it. A team moving off Slack needs similar channels and threads, and a family needs good phone apps. + +- **Phone apps and notifications.** Check the official links for iOS and Android apps. Push notifications often go through a relay the publisher runs, so read how that works. +- **Federation.** Matrix and XMPP servers can talk to other servers. Others only connect your own users. Decide which you want. +- **Paid editions.** Some chat servers keep features such as single sign-on in a paid edition. Check the License and Source model filters, then the feature list. +- **Resources.** A federated server can grow large once users join big public rooms. Treat the RAM and Storage estimates as a starting point. + +## What should you plan for when you run one? + +People use chat all day, so updates and uptime matter. Back up the database and uploaded files. Notification servers are a different kind of app and much lighter, so they often run beside your other apps. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/communication/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/communication/intro.mdx index 48f046b7c..6ca6b3f4f 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/communication/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/communication/intro.mdx @@ -1 +1 @@ -Want a Slack or Discord alternative that keeps messages on your own server? This list has chat servers such as Matrix Synapse, Rocket.Chat, Mattermost, and Zulip, private messengers such as SimpleX Chat, and notification servers such as ntfy and Apprise. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Want a Slack or Discord alternative that keeps messages on your own server? This list has chat servers such as Matrix Synapse, Rocket.Chat, Mattermost, and Zulip, private messengers such as SimpleX Chat, and notification servers such as ntfy and Apprise. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/communities-social/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/communities-social/about.mdx new file mode 100644 index 000000000..8d4835e1a --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/communities-social/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a forum or social server? + +Decide whether you want a standalone community or one that joins the Fediverse, the network of servers whose users can follow each other. + +- **Federation.** A Fediverse server connects your members to people elsewhere. It also pulls in outside posts that you'll need to moderate. +- **Moderation tools.** Check for reporting, blocklists, and spam controls before you open signups. +- **Email.** Forums send signup and notification emails, so you'll need a working mail sender. +- **Storage.** Fediverse servers keep copies of remote media and need regular cleanup. The Storage filter shows which expect a lot of space. + +## What should you plan for when you run one? + +A public community has to be reachable from the internet, so plan for a domain, HTTPS, and regular updates. Back up the database and uploads. Rules on user content and personal data vary by country, so check yours before you open to the public. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/communities-social/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/communities-social/intro.mdx index 337f63731..c0fb29dce 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/communities-social/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/communities-social/intro.mdx @@ -1 +1 @@ -Building a community and want to own the place it lives? This list has forums such as Discourse, Flarum, and NodeBB, Q&A sites such as Apache Answer, and Fediverse servers such as Mastodon, Lemmy, Misskey, and PeerTube. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Building a community and want to own the place it lives? This list has forums such as Discourse, Flarum, and NodeBB, Q&A sites such as Apache Answer, and Fediverse servers such as Mastodon, Lemmy, Misskey, and PeerTube. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/content-publishing/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/content-publishing/about.mdx new file mode 100644 index 000000000..56df070db --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/content-publishing/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a CMS or blog platform? + +First decide how the site gets built. A blogging platform gives you the editor and the site in one. A headless CMS stores content for a front end you build. A static site generator turns files into a site, with no app running on a server at all. + +- **Who writes.** Writers who don't code need a web editor. If only you write, Markdown files in Git can be enough. +- **Database.** Most blogging platforms and headless CMSs need one. Static generators don't. The Needs database signal shows the ones we've confirmed. +- **Install method.** Some only ship as a binary or a system package, not Docker. Check the Install method filter. +- **Themes and plugins.** These can carry their own licenses, so check them if you build sites for clients. + +## What should you plan for when you run one? + +A public website faces the internet all the time, so keep the app and its plugins updated. Back up the database and the media folder. A static site needs neither and can go on almost any web host. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/content-publishing/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/content-publishing/intro.mdx index 712e016b5..7fa9c9583 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/content-publishing/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/content-publishing/intro.mdx @@ -1 +1 @@ -Need a website or blog you control end to end? This list has blogging platforms such as Ghost and WordPress, headless CMSs such as Strapi, Payload, and Directus, and static site generators such as Hugo and Zola. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Need a website or blog you control end to end? This list has blogging platforms such as Ghost and WordPress, headless CMSs such as Strapi, Payload, and Directus, and static site generators such as Hugo and Zola. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/dashboards/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/dashboards/about.mdx new file mode 100644 index 000000000..c1a2f67b9 --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/dashboards/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a dashboard app? + +This category covers three different jobs, so name yours first: a start page for your services, charts from your data, or web analytics. + +- **Start pages** should be light and easy to configure. Use the RAM filter, and check for widgets that show the status of the services you run. +- **Data dashboards** connect to your databases. Check that the app supports yours and can limit what each user sees. +- **Web analytics** collect visitor data. Check whether it uses cookies, since that decides whether you need a consent banner in some countries. +- **Health.** Some dashboards are small one-person projects. The Health filter shows which still get updates. + +## What should you plan for when you run one? + +A start page often holds API keys for your other services, so keep it behind a login or a VPN. An analytics app must be reachable from the internet to count visits, and its database grows with your traffic. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/dashboards/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/dashboards/intro.mdx index 7260ad752..0cf454b95 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/dashboards/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/dashboards/intro.mdx @@ -1 +1 @@ -Want one page for your services, or charts for your data and site traffic? This list has homelab start pages such as Homepage and Glance, data and BI dashboards such as Grafana, Metabase, and Superset, and web analytics such as Umami and PostHog. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Want one page for your services, or charts for your data and site traffic? This list has homelab start pages such as Homepage and Glance, data and BI dashboards such as Grafana, Metabase, and Superset, and web analytics such as Umami and PostHog. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/databases/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/databases/about.mdx new file mode 100644 index 000000000..ce34295d0 --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/databases/about.mdx @@ -0,0 +1,11 @@ +## How do you choose a database or backend? + +Usually the app you're building or running decides. Most apps only work with one or two databases, such as Postgres or MySQL. When you do have a choice, match the type to your data: relational, key-value, document, graph, or search. + +- **License.** Some database projects have changed to source-available licenses over the years. Check the Source model filter, especially if you plan to offer the database as a service. +- **RAM.** Databases use spare memory for caching and get faster with more. Treat the RAM estimate as a minimum. +- **Backend platforms** bundle a database with logins, file storage, and APIs. They save setup, but they run several services at once. + +## What should you plan for when you run one? + +Backups are most of the job. Use the database's own dump or snapshot tool instead of copying files while it runs, and test a restore. Keep its port off the public internet, and let apps connect over a private network. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/databases/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/databases/intro.mdx index 985adb26e..a456f3271 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/databases/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/databases/intro.mdx @@ -1 +1 @@ -Need a database or a full backend on your own server? This list has database servers such as MariaDB, Redis, Neo4j, and Elasticsearch, Postgres extensions such as PostGIS and pgvector, and backend platforms such as Supabase, PocketBase, and Appwrite. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Need a database or a full backend on your own server? This list has database servers such as MariaDB, Redis, Neo4j, and Elasticsearch, Postgres extensions such as PostGIS and pgvector, and backend platforms such as Supabase, PocketBase, and Appwrite. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/design-creative/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/design-creative/about.mdx new file mode 100644 index 000000000..52f3c30de --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/design-creative/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a design or whiteboard app? + +Start with how many people work together. A team needs live collaboration, while one designer can pick a lighter tool. + +- **File formats.** Check what you can import and export, such as SVG, PNG, or PDF, and whether it opens files from the tool you're leaving. +- **Collaboration.** Live editing often needs an extra service for shared sessions. Read the install docs for everything you'll have to run. +- **GPU for image generation.** Image generators need a capable graphics card. The RAM estimate doesn't cover its video memory. +- **Model licenses.** Image models have their own licenses. Check them before you use the results commercially. + +## What should you plan for when you run one? + +Back up the files and boards people create. Some whiteboards keep drawings in your browser unless you set up storage on the server, so check where your work is saved before you rely on it. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/design-creative/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/design-creative/intro.mdx index f0f802168..62faed748 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/design-creative/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/design-creative/intro.mdx @@ -1 +1 @@ -Want a Figma or Miro alternative on your own server? This list has design tools such as Penpot, whiteboards such as Excalidraw and tldraw, diagram servers such as PlantUML, slide tools such as reveal.js, and image generators such as Stable Diffusion web UI. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Want a Figma or Miro alternative on your own server? This list has design tools such as Penpot, whiteboards such as Excalidraw and tldraw, diagram servers such as PlantUML, slide tools such as reveal.js, and image generators such as Stable Diffusion web UI. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/developer-tools/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/developer-tools/about.mdx new file mode 100644 index 000000000..47ba2c9ee --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/developer-tools/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a self-hosted developer tool? + +This category is broad, so search for the job first. Then check: + +- **Who uses it.** A tool for a team needs accounts, single sign-on, and access controls. A tool only you use can skip them. +- **Resources.** CI runners, browser IDEs, and load testers can use a lot of CPU and RAM when they're busy. Treat the RAM estimate as a minimum. +- **License.** Some developer platforms keep features in a paid edition or use a source-available license. Check the Source model filter before you build on one. +- **Install method.** Some tools are made for Kubernetes or Helm. Use the Install method filter to match your setup. + +## What should you plan for when you run one? + +These tools often hold source code, secrets, and deploy keys. Keep them behind a login, update them quickly, and back up their data, such as repositories, registries, and databases. Coding agents can run commands, so give them a sandbox and only the access they need. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/developer-tools/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/developer-tools/intro.mdx index b97458bd9..ccb01a4ce 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/developer-tools/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/developer-tools/intro.mdx @@ -1 +1 @@ -Want your development tools on your own server instead of a paid SaaS? This list has browser IDEs such as VS Code, coding agents such as OpenCode, container registries, API gateways such as Kong, queues such as RabbitMQ, and test tools such as Locust and LocalStack. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Want your development tools on your own server instead of a paid SaaS? This list has browser IDEs such as VS Code, coding agents such as OpenCode, container registries, API gateways such as Kong, queues such as RabbitMQ, and test tools such as Locust and LocalStack. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/documents-pdfs/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/documents-pdfs/about.mdx new file mode 100644 index 000000000..d33648623 --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/documents-pdfs/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a document or PDF app? + +Decide whether you need to store documents, edit them, or change them, such as merging or compressing PDFs. + +- **Document archives** scan, read, and tag your paper. Check OCR (text recognition) support for your languages and whether it can watch a folder for new scans. +- **PDF tools** do the work either in your browser or on the server. If privacy is why you self-host, check which one it is. +- **Office suites** usually plug into a file server rather than run on their own. Check that they work with the one you use. +- **E-signature.** Check what audit trail it keeps, and whether its signatures meet the rules where you sign. + +## What should you back up? + +An archive often becomes the only copy of your paperwork. Back up the original files, the database, and the search index together. Use the Storage filter to plan for years of scans, and keep the app off the public internet unless you need to share from it. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/documents-pdfs/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/documents-pdfs/intro.mdx index 2d4484e42..1ecfba00a 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/documents-pdfs/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/documents-pdfs/intro.mdx @@ -1 +1 @@ -Drowning in paper, PDFs, or office files you would rather not upload anywhere? This list has document archives such as Paperless-ngx, PDF tools such as Stirling-PDF and BentoPDF, office suites such as Collabora, e-signature apps such as Docuseal, and Overleaf for LaTeX. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Drowning in paper, PDFs, or office files you would rather not upload anywhere? This list has document archives such as Paperless-ngx, PDF tools such as Stirling-PDF and BentoPDF, office suites such as Collabora, e-signature apps such as Docuseal, and Overleaf for LaTeX. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/ebooks-libraries/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/ebooks-libraries/about.mdx new file mode 100644 index 000000000..a79215fa6 --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/ebooks-libraries/about.mdx @@ -0,0 +1,12 @@ +## How do you choose an e-book or comic server? + +Start with what you read and on which device. + +- **Formats.** Check support for your files, such as EPUB, PDF, CBZ comics, or audiobook formats. +- **Reader apps.** Many servers offer OPDS, a catalog feed that e-reader apps can browse and download from. Check for it if you read in an app rather than a browser. +- **Your existing library.** If you already use Calibre on a computer, some servers can read its library folder directly. +- **Family use.** Check for separate accounts and reading progress for each person. + +## What should you back up? + +The book files are the big part, so check the Storage filter. Back up the app's database too, since it holds reading progress, collections, and your metadata edits. If you want the server on a NAS, use the RAM filter to find one that fits. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/ebooks-libraries/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/ebooks-libraries/intro.mdx index 3408aaf2e..0b70aa9c6 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/ebooks-libraries/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/ebooks-libraries/intro.mdx @@ -1 +1 @@ -Have a shelf of e-books, comics, or audiobooks you want to read anywhere? This list has e-book servers such as Calibre-Web and Kavita, comic readers such as Komga, audiobook servers such as Audiobookshelf, and collection managers such as RomM. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Have a shelf of e-books, comics, or audiobooks you want to read anywhere? This list has e-book servers such as Calibre-Web and Kavita, comic readers such as Komga, audiobook servers such as Audiobookshelf, and collection managers such as RomM. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/email-newsletters/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/email-newsletters/about.mdx new file mode 100644 index 000000000..587c6a6ca --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/email-newsletters/about.mdx @@ -0,0 +1,14 @@ +## How do you choose a mail server or newsletter app? + +A mail server is one of the hardest things to self-host. Mail from home connections and new IP addresses often lands in spam or gets rejected, so check your network before you pick an app. + +- **Your host.** Many home internet providers and some VPS hosts block port 25, which mail servers need. Ask yours first. +- **All-in-one or parts.** Some bundles include sending, mailboxes, spam filtering, and webmail. Putting parts together gives more control but more work. +- **Newsletters.** A newsletter app usually sends through an outside sending service. Check which ones it supports. +- **RAM.** Spam and virus filters use real memory. Treat the RAM estimate as a minimum. + +Test inboxes are a separate kind of tool. They catch mail from your apps during development and never deliver it. + +## What do you need to set up? + +You'll need SPF, DKIM, and DMARC records on your domain, plus a reverse DNS name for your server's IP. Back up mailboxes and settings, and watch your sending reputation. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/email-newsletters/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/email-newsletters/intro.mdx index 6dc50f898..98c98a604 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/email-newsletters/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/email-newsletters/intro.mdx @@ -1 +1 @@ -Want to run your own mail or send newsletters without a per-subscriber bill? This list has mail servers such as Mailcow, Stalwart, and Mail-in-a-Box, webmail such as Roundcube, newsletter senders such as Listmonk, and test inboxes such as Mailpit. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Want to run your own mail or send newsletters without a per-subscriber bill? This list has mail servers such as Mailcow, Stalwart, and Mail-in-a-Box, webmail such as Roundcube, newsletter senders such as Listmonk, and test inboxes such as Mailpit. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/file-sharing-sync/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/file-sharing-sync/about.mdx new file mode 100644 index 000000000..5719da6d1 --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/file-sharing-sync/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a file sync or sharing app? + +Decide whether you want files to sync between devices, to share links with other people, or to move and download big files. + +- **Sync clients.** Real sync needs desktop and phone apps. Check the app's links for clients on every device you use. +- **How files are stored.** Some apps keep files as normal folders on disk. Others use their own format. Normal folders are easier to back up and to leave. +- **Resources.** Full suites run a database and background jobs. Single-purpose tools are much lighter. Compare them with the RAM filter. +- **Sharing.** For public links, check for expiry dates, passwords, and upload limits. + +## What should you know before you run one? + +Sync is not a backup. A deleted or encrypted file syncs to every device, so keep separate backups with older versions. If you share public links, the app faces the internet, so keep it updated. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/file-sharing-sync/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/file-sharing-sync/intro.mdx index 5cc68e2de..f1d005eef 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/file-sharing-sync/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/file-sharing-sync/intro.mdx @@ -1 +1 @@ -Want a Dropbox or Google Drive alternative, or a better way to move big files? This list has sync tools such as Nextcloud and Syncthing, sharing servers such as copyparty and AList, transfer tools such as Rclone, and download clients such as qBittorrent. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Want a Dropbox or Google Drive alternative, or a better way to move big files? This list has sync tools such as Nextcloud and Syncthing, sharing servers such as copyparty and AList, transfer tools such as Rclone, and download clients such as qBittorrent. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/finance-budgeting/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/finance-budgeting/about.mdx new file mode 100644 index 000000000..5d87d12c0 --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/finance-budgeting/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a budgeting or finance app? + +Start with how your transactions get in. Typing each one by hand works for some people and is why others give up. + +- **Bank import.** Check whether it imports CSV, OFX, or QIF files from your bank, or connects to a bank sync service. Bank sync is often paid or limited to some countries. +- **Budget style.** Some apps use envelope budgeting, where you plan every bit of money before you spend it. Others track spending after it happens. +- **Invoicing.** If you bill clients, check tax, currency, and payment support for where you work. +- **Crypto nodes** are a different kind of app. A full node needs a lot of disk and stays online all the time, so check the Storage filter. + +## What should you back up? + +Your financial history is hard to rebuild. Back up the database often, and keep the app behind a login or a VPN rather than open to the internet. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/finance-budgeting/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/finance-budgeting/intro.mdx index 79d1183cd..aee018caf 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/finance-budgeting/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/finance-budgeting/intro.mdx @@ -1 +1 @@ -Want to track your money without handing bank data to a budgeting app? This list has budget apps such as Actual Budget and Firefly III, invoicing such as Invoice Ninja, market research such as OpenBB, and cryptocurrency nodes such as Bitcoin Core and Geth. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Want to track your money without handing bank data to a budgeting app? This list has budget apps such as Actual Budget and Firefly III, invoicing such as Invoice Ninja, market research such as OpenBB, and cryptocurrency nodes such as Bitcoin Core and Geth. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/forms-polls-events/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/forms-polls-events/about.mdx new file mode 100644 index 000000000..3c85fa8b7 --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/forms-polls-events/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a forms or events app? + +Decide what you're collecting: survey answers, signups, meeting times, or ticket payments. + +- **Questions and logic.** Check for the field types and branching you need, and whether you can embed forms on your own site. +- **Payments.** For paid events, check which payment providers it supports and how it handles refunds and tax. +- **Spam.** Public forms attract bots. Look for a captcha or rate limiting. +- **Getting answers out.** Check that you can export responses to CSV or send them to other apps with webhooks. + +## What should you plan for when you run one? + +People fill in forms from anywhere, so the app has to face the internet. Keep it updated. Responses often include names and emails, so back up the database and check the privacy rules for the people you collect from. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/forms-polls-events/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/forms-polls-events/intro.mdx index bb1c5c38f..d6dc1cae7 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/forms-polls-events/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/forms-polls-events/intro.mdx @@ -1 +1 @@ -Need a Typeform, Google Forms, or Eventbrite alternative you control? This list has form builders such as Formbricks, HeyForm, and OpnForm, surveys such as LimeSurvey, scheduling polls such as Rallly, and event ticketing such as Pretix and Hi.Events. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Need a Typeform, Google Forms, or Eventbrite alternative you control? This list has form builders such as Formbricks, HeyForm, and OpnForm, surveys such as LimeSurvey, scheduling polls such as Rallly, and event ticketing such as Pretix and Hi.Events. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/games/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/games/about.mdx new file mode 100644 index 000000000..238ef1365 --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/games/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a game server or game app? + +Start with the game and how many people will play. + +- **Players and RAM.** A game server uses more memory as players and mods grow. Treat the RAM estimate as the floor for a small group. +- **License.** Many game servers are closed source but free to run, while each player still needs the game. The Source model filter shows which are open. +- **Getting friends in.** Players outside your home need a way to reach the server: an open port, a tunnel, or a VPN they join. +- **Game streaming** needs a GPU in the host and a fast local network, not just a server. + +## What should you back up? + +Back up world saves before every update or mod change, since a bad update can break a world. Game library and ROM managers hold large collections, so check the Storage filter before you pick one. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/games/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/games/intro.mdx index 4730d469a..af1df0a50 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/games/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/games/intro.mdx @@ -1 +1 @@ -Want to host a game server for friends or keep your game library in one place? This list has game servers such as Minecraft, Factorio, and Luanti, Foundry VTT for tabletop games, Sunshine for streaming games to other devices, and RomM for your ROM collection. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Want to host a game server for friends or keep your game library in one place? This list has game servers such as Minecraft, Factorio, and Luanti, Foundry VTT for tabletop games, Sunshine for streaming games to other devices, and RomM for your ROM collection. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/health-fitness/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/health-fitness/about.mdx new file mode 100644 index 000000000..ffa839f0e --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/health-fitness/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a health or fitness app? + +Health data is personal, and keeping it private is often the reason to self-host it. Check: + +- **Phone use.** You'll log workouts and meals on your phone. Check the official links for a phone app or a web app that works well on a small screen. +- **Imports.** Look for import from the app or device you use now, such as a CSV export or a watch sync. +- **Family use.** Check for separate accounts for each person. +- **Medical use.** Glucose and health record tools usually come with warnings about medical use. Read them before you rely on one for care decisions. + +## What should you back up? + +Back up the database, and keep the app behind a login or a VPN. If you open it to the internet for a phone app, use HTTPS and strong passwords. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/health-fitness/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/health-fitness/intro.mdx index 88717e96f..7a4f51e15 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/health-fitness/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/health-fitness/intro.mdx @@ -1 +1 @@ -Want your workout and health data to stay private? This list has workout trackers such as wger and Workout.cool, glucose monitoring such as Nightscout, health record apps such as Fasten Health, and Baby Buddy for tracking a newborn's feeds and sleep. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Want your workout and health data to stay private? This list has workout trackers such as wger and Workout.cool, glucose monitoring such as Nightscout, health record apps such as Fasten Health, and Baby Buddy for tracking a newborn's feeds and sleep. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/home-automation-iot/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/home-automation-iot/about.mdx new file mode 100644 index 000000000..408e25877 --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/home-automation-iot/about.mdx @@ -0,0 +1,12 @@ +## How do you choose home automation apps? + +Start with your devices. The way they talk decides much of the rest. + +- **Protocols.** Zigbee and Z-Wave devices need a USB radio or a hub. Wi-Fi devices may need a cloud account unless the app supports local control. +- **Install method.** Some hubs run best as their own operating system. The Install method filter shows options such as Home Assistant OS and Docker. +- **Hardware access.** In Docker, a USB radio has to be passed through to the container. Check the install docs. +- **Integrations.** Check the app's integration list for your brands before you buy more devices. + +## What should you plan for when you run one? + +A hub runs your lights and locks, so give it a stable machine that stays on, such as a small PC or a Raspberry Pi. Back up its config before updates. Keep it off the open internet, and use a VPN to reach it from outside. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/home-automation-iot/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/home-automation-iot/intro.mdx index 69266bbf0..ba7b033a3 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/home-automation-iot/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/home-automation-iot/intro.mdx @@ -1 +1 @@ -Want a smart home that runs locally instead of through cloud accounts? This list has hubs such as Home Assistant, automation tools such as Node-RED, device bridges such as Zigbee2MQTT and Homebridge, firmware tools such as ESPHome, and MQTT brokers such as Mosquitto. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Want a smart home that runs locally instead of through cloud accounts? This list has hubs such as Home Assistant, automation tools such as Node-RED, device bridges such as Zigbee2MQTT and Homebridge, firmware tools such as ESPHome, and MQTT brokers such as Mosquitto. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/identity-access/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/identity-access/about.mdx new file mode 100644 index 000000000..49ed7e006 --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/identity-access/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a login or SSO server? + +List the apps you want behind one login, then check: + +- **Protocols.** Most apps support OIDC. Some only support SAML or LDAP. Make sure the server speaks what your apps need. +- **Proxy login.** Apps with no login of their own can sit behind forward auth, where your reverse proxy asks the login server first. Check it works with your proxy. +- **Two-factor.** Check for passkeys and authenticator app codes. +- **Resources.** Some login servers are small, single-file apps. Others need a database and a cache beside them. Compare with the RAM filter and the Needs database signal. + +## What should you plan for when you run one? + +This becomes the front door to everything else. If it goes down, so do your logins, so keep a way back in, such as a local admin account. Back up its database and keys, and update it quickly when security fixes come out. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/identity-access/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/identity-access/intro.mdx index ea975dfd5..318438b69 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/identity-access/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/identity-access/intro.mdx @@ -1 +1 @@ -Running several apps and want one secure login in front of them? This list has single sign-on servers such as Authentik, Keycloak, Authelia, and Zitadel, auth proxies such as OAuth2 Proxy, and access gateways such as Pangolin and Teleport. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Running several apps and want one secure login in front of them? This list has single sign-on servers such as Authentik, Keycloak, Authelia, and Zitadel, auth proxies such as OAuth2 Proxy, and access gateways such as Pangolin and Teleport. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/index.ts b/app/(site)/(directories)/self-host-apps/categories/[category]/content/index.ts index 3c84e6cc0..314ebd74a 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/index.ts +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/index.ts @@ -41,7 +41,50 @@ import Intro38 from "./system-admin-hosting/intro.mdx"; import Intro39 from "./time-tracking/intro.mdx"; import Intro40 from "./video-conferencing/intro.mdx"; import Intro41 from "./wikis/intro.mdx"; +import About0 from "./ai-ml/about.mdx"; +import About1 from "./automation/about.mdx"; +import About2 from "./bookmarks-read-it-later/about.mdx"; +import About3 from "./business-commerce/about.mdx"; +import About4 from "./calendars-contacts/about.mdx"; +import About5 from "./communication/about.mdx"; +import About6 from "./communities-social/about.mdx"; +import About7 from "./content-publishing/about.mdx"; +import About8 from "./dashboards/about.mdx"; +import About9 from "./databases/about.mdx"; +import About10 from "./design-creative/about.mdx"; +import About11 from "./developer-tools/about.mdx"; +import About12 from "./documents-pdfs/about.mdx"; +import About13 from "./ebooks-libraries/about.mdx"; +import About14 from "./email-newsletters/about.mdx"; +import About15 from "./file-sharing-sync/about.mdx"; +import About16 from "./finance-budgeting/about.mdx"; +import About17 from "./forms-polls-events/about.mdx"; +import About18 from "./games/about.mdx"; +import About19 from "./health-fitness/about.mdx"; +import About20 from "./home-automation-iot/about.mdx"; +import About21 from "./identity-access/about.mdx"; +import About22 from "./knowledge-notes/about.mdx"; +import About23 from "./learning-courses/about.mdx"; +import About24 from "./maps-travel/about.mdx"; +import About25 from "./media-device-control/about.mdx"; +import About26 from "./media-servers/about.mdx"; +import About27 from "./monitoring-status/about.mdx"; +import About28 from "./music-audio/about.mdx"; +import About29 from "./networking-dns/about.mdx"; +import About30 from "./password-managers/about.mdx"; +import About31 from "./photos-galleries/about.mdx"; +import About32 from "./project-task-management/about.mdx"; +import About33 from "./recipes-food/about.mdx"; +import About34 from "./remote-access/about.mdx"; +import About35 from "./search/about.mdx"; +import About36 from "./security-privacy/about.mdx"; +import About37 from "./storage-backup/about.mdx"; +import About38 from "./system-admin-hosting/about.mdx"; +import About39 from "./time-tracking/about.mdx"; +import About40 from "./video-conferencing/about.mdx"; +import About41 from "./wikis/about.mdx"; +// One intro (above the apps) and one about section (below the apps) per category. export const categoryIntros: Record = { "ai-ml": Intro0, "automation": Intro1, @@ -86,3 +129,48 @@ export const categoryIntros: Record = { "video-conferencing": Intro40, "wikis": Intro41, }; + +export const categoryAbouts: Record = { + "ai-ml": About0, + "automation": About1, + "bookmarks-read-it-later": About2, + "business-commerce": About3, + "calendars-contacts": About4, + "communication": About5, + "communities-social": About6, + "content-publishing": About7, + "dashboards": About8, + "databases": About9, + "design-creative": About10, + "developer-tools": About11, + "documents-pdfs": About12, + "ebooks-libraries": About13, + "email-newsletters": About14, + "file-sharing-sync": About15, + "finance-budgeting": About16, + "forms-polls-events": About17, + "games": About18, + "health-fitness": About19, + "home-automation-iot": About20, + "identity-access": About21, + "knowledge-notes": About22, + "learning-courses": About23, + "maps-travel": About24, + "media-device-control": About25, + "media-servers": About26, + "monitoring-status": About27, + "music-audio": About28, + "networking-dns": About29, + "password-managers": About30, + "photos-galleries": About31, + "project-task-management": About32, + "recipes-food": About33, + "remote-access": About34, + "search": About35, + "security-privacy": About36, + "storage-backup": About37, + "system-admin-hosting": About38, + "time-tracking": About39, + "video-conferencing": About40, + "wikis": About41, +}; diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/knowledge-notes/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/knowledge-notes/about.mdx new file mode 100644 index 000000000..a65523919 --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/knowledge-notes/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a notes app? + +Start with where your notes are stored and how you'd move them out if you had to. + +- **Storage format.** Some apps keep notes as Markdown files. Others keep them in a database. Plain files are easier to move and back up. +- **Sync and offline.** Check how the phone and desktop apps sync, and whether you can write offline. +- **Encryption.** If the server runs on someone else's machine, end-to-end encryption keeps your notes private from it. +- **Paid features.** A few apps keep sync or collaboration in a paid edition. Check the Source model filter and the feature list. + +## What should you back up? + +Notes pile up over years. Back up the notes, attachments, and database together. Try an export once, so you know you can leave if the project stops. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/knowledge-notes/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/knowledge-notes/intro.mdx index 9ebc0c134..3a29616a2 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/knowledge-notes/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/knowledge-notes/intro.mdx @@ -1 +1 @@ -Want a Notion or Evernote alternative where your notes stay yours? This list has note apps such as Joplin and Memos, outliners such as Logseq, knowledge bases such as SiYuan and Trilium Notes, and all-in-one workspaces such as AFFiNE. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Want a Notion or Evernote alternative where your notes stay yours? This list has note apps such as Joplin and Memos, outliners such as Logseq, knowledge bases such as SiYuan and Trilium Notes, and all-in-one workspaces such as AFFiNE. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/learning-courses/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/learning-courses/about.mdx new file mode 100644 index 000000000..625c24d4b --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/learning-courses/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a course or learning app? + +Decide who's learning: a class, paying customers, or just you. + +- **Course platforms** handle lessons, quizzes, and grades. The big ones take real setup and memory, so check the RAM filter first. +- **Standards.** If you reuse course content from other systems, check for SCORM or LTI support. +- **Selling courses.** Check that it takes payments, or plan to add them yourself. +- **Personal tools,** such as flashcard sync or language readers, are a different job. They have one user and need far less setup. + +## What should you plan for when you run one? + +Student work, grades, and videos need regular backups. Video takes the most space, so check the Storage filter or host videos somewhere else. A course site faces the internet, so keep it updated. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/learning-courses/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/learning-courses/intro.mdx index 11509241d..7351356bf 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/learning-courses/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/learning-courses/intro.mdx @@ -1 +1 @@ -Teaching a class, running courses, or studying on your own? This list has course platforms such as Open edX, Canvas LMS, and LearnHouse, an Anki sync server for flashcards, language learning tools such as LinguaCafe and Lute, and coding sandboxes such as Judge0. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Teaching a class, running courses, or studying on your own? This list has course platforms such as Open edX, Canvas LMS, and LearnHouse, an Anki sync server for flashcards, language learning tools such as LinguaCafe and Lute, and coding sandboxes such as Judge0. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/maps-travel/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/maps-travel/about.mdx new file mode 100644 index 000000000..7a1182c4a --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/maps-travel/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a maps or location app? + +Decide whether you want to track where you go, plan routes, or serve your own maps. + +- **Phone tracking.** A location tracker needs an app on your phone sending your position. Check which phone apps the server accepts. +- **Map data.** Routing engines and tile servers use OpenStreetMap data. One region fits on a modest server, but the whole planet needs a lot of disk and memory. +- **Import.** If you're leaving Google Maps, check for import of your Google Takeout location history. +- **Resources.** Preparing routing data takes far more memory than serving it. Treat the RAM estimate as a minimum. + +## What should you plan for when you run one? + +Location history is sensitive. Keep the app behind HTTPS and a login, and back up its database. Map data can be downloaded again, so it matters less in your backups. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/maps-travel/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/maps-travel/intro.mdx index 1ce3311c0..b0ae665d5 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/maps-travel/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/maps-travel/intro.mdx @@ -1 +1 @@ -Want your location history and trip plans off Google Maps? This list has location trackers such as Dawarich and Traccar, routing engines such as OSRM, GraphHopper, and Valhalla, map tile servers, and car loggers such as TeslaMate. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Want your location history and trip plans off Google Maps? This list has location trackers such as Dawarich and Traccar, routing engines such as OSRM, GraphHopper, and Valhalla, map tile servers, and car loggers such as TeslaMate. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/media-device-control/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/media-device-control/about.mdx new file mode 100644 index 000000000..8a0f662fd --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/media-device-control/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a media control app? + +Start with the devices you want to play on: speakers, TVs, cameras, or a photo frame. + +- **Device support.** Check which devices and protocols it works with, such as Chromecast, AirPlay, or DLNA. +- **Where it runs.** Some apps run on the playback device itself, such as a media center on a TV box. Others are a server that sends to many devices. +- **Multi-room sync.** For music in several rooms, check that it keeps the rooms playing in time. +- **Smart home.** Some of these plug into a home automation hub. Check the install docs if you want to control playback from yours. + +## What should you plan for when you run one? + +These apps have to find devices on your local network, so Docker setups often need host networking. They store little data, so backups are mostly config files. Keep them off the open internet. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/media-device-control/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/media-device-control/intro.mdx index cc87d8a19..b5034a1bd 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/media-device-control/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/media-device-control/intro.mdx @@ -1 +1 @@ -Want to play music in every room or put the right thing on each screen? This list has media centers such as Kodi, multi-room audio such as Snapcast and Music Assistant, stream relays such as go2rtc, and photo frame apps such as Immich Kiosk. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Want to play music in every room or put the right thing on each screen? This list has media centers such as Kodi, multi-room audio such as Snapcast and Music Assistant, stream relays such as go2rtc, and photo frame apps such as Immich Kiosk. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/media-servers/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/media-servers/about.mdx new file mode 100644 index 000000000..7b4870002 --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/media-servers/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a media server? + +Start with where you'll watch. Then check: + +- **Client apps.** Check for apps on your TV, phone, and streaming stick. They decide more of your daily experience than the server does. +- **Transcoding.** When a device can't play a file, the server converts it on the fly. A GPU or a CPU with built-in video encoding makes that much lighter. Some servers keep hardware transcoding behind a paid plan. +- **License.** Some well-known media servers are closed source with paid tiers. The Source model filter shows which. +- **Watching away from home.** Check whether it streams through the publisher's relay or needs your own remote setup. + +## What should you plan for? + +Media takes the most space, so plan your disks first. Back up the server's database, which holds watch history and metadata, apart from the media files. Collection managers work with download clients and indexers, so check what each expects before you build the stack. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/media-servers/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/media-servers/intro.mdx index 61952f143..34645bc84 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/media-servers/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/media-servers/intro.mdx @@ -1 +1 @@ -Building your own Netflix for the movies and shows you own? This list has media servers such as Jellyfin, Plex, and Emby, collection managers such as Sonarr and Radarr, indexer tools such as NZBHydra 2, request apps, subtitle tools, and transcoders. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Building your own Netflix for the movies and shows you own? This list has media servers such as Jellyfin, Plex, and Emby, collection managers such as Sonarr and Radarr, indexer tools such as NZBHydra 2, request apps, subtitle tools, and transcoders. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/monitoring-status/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/monitoring-status/about.mdx new file mode 100644 index 000000000..d91ec4d6c --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/monitoring-status/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a monitoring app? + +Decide what you need to know: whether a site is up, how a server is doing, or what went wrong in the logs. + +- **Uptime checks** should run somewhere other than the servers they watch. Otherwise they go down together and nobody tells you. +- **Alerts.** Check that it can reach you where you'll see it, such as email, chat, or push notifications. +- **Metrics and logs** grow fast. Check how long it keeps data, and use the Storage filter. +- **Agents.** Many tools need a small agent on each server. Check it supports your systems. + +## What should you plan for when you run one? + +A metrics setup is often several apps together, such as a collector, a database, and a dashboard, so add up their RAM. A public status page faces the internet, so keep the admin side private. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/monitoring-status/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/monitoring-status/intro.mdx index 44c6c9d3a..87f71750e 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/monitoring-status/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/monitoring-status/intro.mdx @@ -1 +1 @@ -Need to know when a site goes down or a server runs out of disk? This list has uptime checkers and status pages such as Uptime Kuma, metrics stacks such as Prometheus and Grafana, live server monitors such as Netdata, and log collectors such as Promtail and Alloy. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Need to know when a site goes down or a server runs out of disk? This list has uptime checkers and status pages such as Uptime Kuma, metrics stacks such as Prometheus and Grafana, live server monitors such as Netdata, and log collectors such as Promtail and Alloy. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/music-audio/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/music-audio/about.mdx new file mode 100644 index 000000000..5be22c6ce --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/music-audio/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a music server? + +Start with your library and the apps you want to listen in. + +- **Client apps.** Many music servers support the Subsonic API, which lets you pick from lots of phone and desktop players. Check for it if you want a choice. +- **Formats.** Check support for your files, such as FLAC or Opus, and whether it can convert to a smaller format on slow connections. +- **Tags.** A library with clean tags looks better in every server. A tagger is worth running before you import. +- **Resources.** Use the RAM filter if you want the server on a small machine like a Raspberry Pi. + +## What should you back up? + +Your music files are the big part, so plan storage first. Back up the server's database too, since it holds playlists, ratings, and play counts. To listen away from home, use HTTPS or a VPN. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/music-audio/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/music-audio/intro.mdx index 9d306bead..2df55c219 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/music-audio/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/music-audio/intro.mdx @@ -1 +1 @@ -Have a music collection and want to stream it like Spotify? This list has music servers such as Navidrome and koel, players such as Feishin, library taggers such as Beets, audiobook and podcast servers such as Audiobookshelf, and audio tools for bots and radio. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Have a music collection and want to stream it like Spotify? This list has music servers such as Navidrome and koel, players such as Feishin, library taggers such as Beets, audiobook and podcast servers such as Audiobookshelf, and audio tools for bots and radio. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/networking-dns/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/networking-dns/about.mdx new file mode 100644 index 000000000..2d229f392 --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/networking-dns/about.mdx @@ -0,0 +1,12 @@ +## How do you choose networking and DNS tools? + +Name the job first: block ads, route traffic to your apps, reach home from outside, or test your network. + +- **DNS blockers** sit in the path of every lookup in your home. If everyone relies on it, run a second one so the internet doesn't stop when one goes down. +- **Reverse proxies** put your apps behind one address with HTTPS. Check for automatic certificates and a way of setting routes that suits you, such as a config file, Docker labels, or a web UI. +- **Tunnels** reach your apps without opening ports. Check whether traffic passes through a server you run or through a company's. +- **Install method.** Some of these tools are a single binary. Check the Install method filter for Native and Binary. + +## What should you plan for when you run one? + +A broken DNS server or proxy takes everything behind it down, so keep a fallback and back up the config. Anything you expose to the internet needs updates and strong logins. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/networking-dns/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/networking-dns/intro.mdx index 6329b29cc..efd0d9206 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/networking-dns/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/networking-dns/intro.mdx @@ -1 +1 @@ -Setting up DNS, a reverse proxy, or a tunnel for your home server? This list has DNS ad blockers such as Pi-hole, reverse proxies such as Traefik, Caddy, and Nginx Proxy Manager, tunnels such as frp, VPN clients, and network test tools such as iperf3. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Setting up DNS, a reverse proxy, or a tunnel for your home server? This list has DNS ad blockers such as Pi-hole, reverse proxies such as Traefik, Caddy, and Nginx Proxy Manager, tunnels such as frp, VPN clients, and network test tools such as iperf3. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/password-managers/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/password-managers/about.mdx new file mode 100644 index 000000000..94b8fc92e --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/password-managers/about.mdx @@ -0,0 +1,14 @@ +## How do you choose a password manager to self-host? + +Once you self-host it, keeping your passwords safe is your job. Check: + +- **Clients.** Check for browser extensions and phone apps. Some servers work with another project's official apps. +- **Encryption.** Look for end-to-end encryption, where the server never sees your vault unlocked. +- **Sharing.** For a family or team, check how shared vaults and permissions work. +- **Health.** Security fixes matter more here than anywhere. Prefer apps the Health filter marks Active. + +Secrets managers are for apps and servers, not people. For those, check API access and audit logs instead. + +## What should you plan for when you run one? + +If the server dies, you'll need your passwords to fix it. Keep an offline export or a copy of the vault on your devices. Back up the database often, test a restore, and update quickly when a security fix comes out. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/password-managers/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/password-managers/intro.mdx index f42352fc8..090a51a07 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/password-managers/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/password-managers/intro.mdx @@ -1 +1 @@ -Want your passwords in a vault you run yourself? This list has password managers such as Vaultwarden, Bitwarden, and Passbolt, secrets managers for apps such as Infisical, HashiCorp Vault, and OpenBao, and one-time sharing tools such as Yopass and Password Pusher. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Want your passwords in a vault you run yourself? This list has password managers such as Vaultwarden, Bitwarden, and Passbolt, secrets managers for apps such as Infisical, HashiCorp Vault, and OpenBao, and one-time sharing tools such as Yopass and Password Pusher. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/photos-galleries/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/photos-galleries/about.mdx new file mode 100644 index 000000000..94dbce87c --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/photos-galleries/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a photo app? + +Decide whether you want automatic backup from your phone, a gallery to show your work, or both. + +- **Phone backup.** Check the app's links for a phone app that uploads in the background. +- **Your existing folders.** Some apps read your photo folders as they are. Others import copies into their own structure. +- **Search and faces.** Smart search and face grouping use machine learning, which needs more RAM and CPU. Compare with the RAM filter. +- **Formats.** Check support for HEIC, RAW, and video. + +## What should you plan for when you run one? + +Photo libraries grow every year, so plan storage first. The app isn't a backup on its own. Keep the original files in a separate backup with older versions, along with the app's database. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/photos-galleries/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/photos-galleries/intro.mdx index d02ec0b7e..03b6d5a2d 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/photos-galleries/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/photos-galleries/intro.mdx @@ -1 +1 @@ -Want a Google Photos or iCloud alternative for your own pictures? This list has photo backup and libraries such as Immich, PhotoPrism, and Ente, galleries such as Lychee and Piwigo, photo sharing such as PixelFed, and tools such as icloudpd to pull photos out of iCloud. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Want a Google Photos or iCloud alternative for your own pictures? This list has photo backup and libraries such as Immich, PhotoPrism, and Ente, galleries such as Lychee and Piwigo, photo sharing such as PixelFed, and tools such as icloudpd to pull photos out of iCloud. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/project-task-management/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/project-task-management/about.mdx new file mode 100644 index 000000000..1fb4c078e --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/project-task-management/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a project or task app? + +Start with how you work: kanban boards, sprints, timelines, or simple to-do lists. + +- **Views.** Check for the views you'll use every day, such as boards, lists, and Gantt charts. +- **Import.** If you're leaving Jira, Asana, or Trello, check for an importer. +- **Paid editions.** Some project tools keep features such as single sign-on or Gantt charts in a paid edition. Check the License and Source model filters, then the feature list. +- **Integrations.** Look for Git, chat, and calendar connections if your team relies on them. + +## What should you plan for when you run one? + +Many of these need a database and send email notifications, so plan for both. The Needs database signal shows the ones we've confirmed. Back up the database and attachments, and use HTTPS if people log in from outside. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/project-task-management/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/project-task-management/intro.mdx index ec918e41d..7d09ef715 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/project-task-management/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/project-task-management/intro.mdx @@ -1 +1 @@ -Want a Jira, Asana, or Trello alternative on your own server? This list has project tools such as Plane, OpenProject, and Huly, issue trackers such as YouTrack, kanban boards such as Wekan and Focalboard, and personal planners such as Super Productivity. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Want a Jira, Asana, or Trello alternative on your own server? This list has project tools such as Plane, OpenProject, and Huly, issue trackers such as YouTrack, kanban boards such as Wekan and Focalboard, and personal planners such as Super Productivity. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/recipes-food/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/recipes-food/about.mdx new file mode 100644 index 000000000..4d70dc4ea --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/recipes-food/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a recipe or pantry app? + +Start with what you'll actually do in the kitchen. + +- **Saving recipes.** Check whether it can pull a recipe from a website link. That saves the most typing. +- **Meal plans and shopping lists.** Check that a meal plan turns into a shopping list you can use on your phone. +- **Pantry tracking** only works if adding items is quick, such as by scanning barcodes. Otherwise you'll stop updating it. +- **Household use.** Check for shared lists and separate accounts. + +## What should you plan for when you run one? + +Back up the database and recipe photos. Use the RAM filter to find one that fits a small home server. If you want the shopping list in the store, you'll need a way in from outside, such as a VPN. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/recipes-food/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/recipes-food/intro.mdx index 6d532ac58..4d3520c72 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/recipes-food/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/recipes-food/intro.mdx @@ -1 +1 @@ -Want all your recipes, meal plans, and shopping lists in one place? This list has recipe managers such as Mealie and Tandoor, pantry and grocery tools such as grocy and KitchenOwl, and Bar Assistant for cocktails. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Want all your recipes, meal plans, and shopping lists in one place? This list has recipe managers such as Mealie and Tandoor, pantry and grocery tools such as grocy and KitchenOwl, and Bar Assistant for cocktails. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/remote-access/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/remote-access/about.mdx new file mode 100644 index 000000000..5b4fa1bcb --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/remote-access/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a remote access app? + +Decide what you need to reach: a whole network, a few web apps, a terminal, or a full desktop. + +- **Mesh VPNs** connect your devices as if they were on one network. Check whether the coordination server runs on your machine or the publisher's. +- **Access gateways** put logins and logs in front of servers. For a team, check for single sign-on and session recording. +- **Remote desktop** needs low lag. Check for client apps on your devices and support for GPU encoding. +- **License.** Some tools here keep features in a paid edition. Check the Source model filter. + +## What should you plan for when you run one? + +These apps are the way into your network, so a hole in them is a hole in everything. Update quickly, use two-factor logins, and limit what each user can reach. Keep a second way in for when the remote tool breaks. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/remote-access/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/remote-access/intro.mdx index 3234320e9..b1331928c 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/remote-access/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/remote-access/intro.mdx @@ -1 +1 @@ -Need to reach your computers and servers from anywhere? This list has mesh VPNs such as NetBird and Tailscale, access gateways such as Pangolin, Teleport, and JumpServer, browser IDEs such as code-server, and remote desktop and streaming such as Sunshine and Neko. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Need to reach your computers and servers from anywhere? This list has mesh VPNs such as NetBird and Tailscale, access gateways such as Pangolin, Teleport, and JumpServer, browser IDEs such as code-server, and remote desktop and streaming such as Sunshine and Neko. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/search/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/search/about.mdx new file mode 100644 index 000000000..f68f26a47 --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/search/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a search app? + +Decide what you're searching: the web, your site's content, or your own files. + +- **Metasearch engines** send your query to other search engines and strip the tracking. If many people share one, those engines may start blocking it. +- **Search servers** index your data for an app or site. Check for client libraries in your language and for typo tolerance. +- **RAM.** Many search engines keep their index in memory, so RAM grows with your data. Treat the RAM estimate as a minimum. +- **AI search** over your files needs a model, which adds GPU or API costs. + +## What should you plan for when you run one? + +An index can be rebuilt from your data, so back up the data first. Keep search servers off the public internet, and require API keys for every app that queries them. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/search/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/search/intro.mdx index e1c4bf47f..d907d2acc 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/search/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/search/intro.mdx @@ -1 +1 @@ -Want private web search, or fast search for your site or documents? This list has metasearch engines such as SearXNG, search servers such as Meilisearch, Typesense, and Elasticsearch, and AI search over your own files such as RAGFlow and PrivateGPT. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Want private web search, or fast search for your site or documents? This list has metasearch engines such as SearXNG, search servers such as Meilisearch, Typesense, and Elasticsearch, and AI search over your own files such as RAGFlow and PrivateGPT. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/security-privacy/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/security-privacy/about.mdx new file mode 100644 index 000000000..f50816ed6 --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/security-privacy/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a security or privacy app? + +This category mixes tools that protect you, test your systems, and watch for problems. Name the job first, then check: + +- **Health.** A security tool that isn't maintained can become your weak spot. Prefer apps the Health filter marks Active. +- **Where your data goes.** Some blockers and private front ends still send your queries to an outside service. Read how each one handles them. +- **Permission.** Point scanners only at systems you own or have permission to test. +- **Resources.** Camera recorders and scanners can be heavy. Treat the RAM estimate as a minimum. + +## What should you plan for when you run one? + +Many of these apps sit in front of everything else, so a failure can cut off your network. Keep a fallback, back up the config, and update quickly. Don't expose admin pages to the internet. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/security-privacy/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/security-privacy/intro.mdx index d7fcd3742..c6a7e2b5d 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/security-privacy/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/security-privacy/intro.mdx @@ -1 +1 @@ -Want to block trackers, lock down your network, or check your own sites? This list has DNS blockers such as AdGuard Home, VPN tools such as Gluetun and Headscale, security toolkits such as CyberChef and Web-Check, private front ends such as SearXNG, and Frigate for local cameras. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Want to block trackers, lock down your network, or check your own sites? This list has DNS blockers such as AdGuard Home, VPN tools such as Gluetun and Headscale, security toolkits such as CyberChef and Web-Check, private front ends such as SearXNG, and Frigate for local cameras. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/storage-backup/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/storage-backup/about.mdx new file mode 100644 index 000000000..2703ce0f5 --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/storage-backup/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a storage or backup app? + +Decide whether you need more storage, backups, or both. They're different jobs. + +- **Backups** should be encrypted and keep older versions, so you can go back to last week. Check which destinations it supports, such as S3 storage or another server. +- **Restores.** Check how you'd restore one file and how you'd restore a whole machine. Test it before you need it. +- **Object storage** gives your apps an S3-compatible bucket. Check the License and Source model filters before you build on one. +- **NAS systems** are full operating systems, not apps. Check the Install method filter to see what runs where. + +## What should you plan for when you run one? + +Keep at least one copy off site, since a backup on the same disk or server fails with it. Watch your backup jobs too. A job that fails silently looks fine until the day you need it. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/storage-backup/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/storage-backup/intro.mdx index 69afb5c86..7ba9f512e 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/storage-backup/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/storage-backup/intro.mdx @@ -1 +1 @@ -Need a backup plan, or more storage you control? This list has backup tools such as restic, Kopia, and Duplicati, object storage such as MinIO and SeaweedFS, NAS systems such as Unraid, and archivers such as ArchiveBox. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Need a backup plan, or more storage you control? This list has backup tools such as restic, Kopia, and Duplicati, object storage such as MinIO and SeaweedFS, NAS systems such as Unraid, and archivers such as ArchiveBox. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/system-admin-hosting/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/system-admin-hosting/about.mdx new file mode 100644 index 000000000..6c13b079f --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/system-admin-hosting/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a server management tool? + +Start with how many servers you run and how you deploy apps. + +- **Container managers or app platforms.** A container manager gives you a web UI for Docker. An app platform builds and deploys your apps from Git. +- **Install method.** Check the Install method filter for Docker, Kubernetes, or native installs that match your setup. +- **Automatic updates.** Update tools can pull in a breaking change and restart an app. Check for a notify-only mode or rules for each app. +- **Paid features.** Some tools keep features in a paid plan. Check the Source model filter. + +## What should you plan for when you run one? + +These tools often control the Docker socket or have root access, so whoever gets in controls the server. Keep them behind a login and a VPN. Back up their settings, and keep your app configs, such as Compose files, in Git. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/system-admin-hosting/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/system-admin-hosting/intro.mdx index 2ae93ee93..c81735cc4 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/system-admin-hosting/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/system-admin-hosting/intro.mdx @@ -1 +1 @@ -Running a home server or VPS and want easier upkeep? This list has container managers such as Portainer and Rancher, app platforms such as Coolify, update tools such as watchtower and What's Up Docker, web servers such as Caddy, and configuration tools such as Ansible. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Running a home server or VPS and want easier upkeep? This list has container managers such as Portainer and Rancher, app platforms such as Coolify, update tools such as watchtower and What's Up Docker, web servers such as Caddy, and configuration tools such as Ansible. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/time-tracking/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/time-tracking/about.mdx new file mode 100644 index 000000000..153bd497e --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/time-tracking/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a time tracker? + +Decide whether you're tracking time for clients or just for yourself. + +- **Billing.** For client work, check for rates per project, reports, and invoice export. +- **How you track.** Some apps use a timer you start and stop. Others record what you do automatically. Pick the one you'll keep using. +- **Where you track from.** Check for a phone app, browser extension, or desktop widget. +- **Privacy.** Automatic trackers record which apps and sites you use. Keep that data on a machine you trust. + +## What should you plan for when you run one? + +Back up the database, since lost hours are lost money. If several people log time, set up HTTPS and separate accounts. Some personal trackers run on your own computer instead of a server. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/time-tracking/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/time-tracking/intro.mdx index 674207d9e..a3ecf4034 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/time-tracking/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/time-tracking/intro.mdx @@ -1 +1 @@ -Need to track billable hours, or see where your day goes? This list has timesheet apps such as Kimai and solidtime, simple trackers such as TimeTagger and Traggo, automatic trackers such as ActivityWatch, and Wakapi for time spent coding. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Need to track billable hours, or see where your day goes? This list has timesheet apps such as Kimai and solidtime, simple trackers such as TimeTagger and Traggo, automatic trackers such as ActivityWatch, and Wakapi for time spent coding. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/video-conferencing/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/video-conferencing/about.mdx new file mode 100644 index 000000000..e51814fba --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/video-conferencing/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a video app? + +Decide whether you need meetings, live streams to an audience, or a video platform to build your own app on. + +- **Bandwidth.** Video uses your server's upload speed, and group calls multiply it. A home connection may not be enough for larger calls. +- **Network setup.** Video often needs UDP ports and a TURN server, a relay that lets people behind strict firewalls join. +- **Joining.** Check whether guests can join from a browser or need to install an app. +- **Recording.** Recording and streaming often need an extra service with more CPU and RAM. + +## What should you plan for when you run one? + +These apps have to be reachable from the internet, so a VPS with good bandwidth is often easier than a home server. Recordings take space, so check the Storage filter. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/video-conferencing/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/video-conferencing/intro.mdx index 252c380af..196cb17be 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/video-conferencing/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/video-conferencing/intro.mdx @@ -1 +1 @@ -Want a Zoom or Twitch alternative that runs on your own server? This list has meeting apps such as Jitsi Meet and BigBlueButton, real-time video platforms such as LiveKit and Janus, live streaming servers such as Owncast and SRS, and screen sharing such as Screego. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Want a Zoom or Twitch alternative that runs on your own server? This list has meeting apps such as Jitsi Meet and BigBlueButton, real-time video platforms such as LiveKit and Janus, live streaming servers such as Owncast and SRS, and screen sharing such as Screego. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/wikis/about.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/wikis/about.mdx new file mode 100644 index 000000000..4de01460d --- /dev/null +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/wikis/about.mdx @@ -0,0 +1,12 @@ +## How do you choose a wiki? + +Start with who writes and how they like to write. + +- **Editor.** Team wikis use a visual editor. Classic wikis often use their own markup. Git-based wikis use Markdown files. +- **Permissions.** For a team, check permissions for spaces or pages and support for single sign-on. +- **Storage.** A wiki on a database is easy to search. A wiki on plain files is easy to move and back up. +- **Search.** Check for full-text search, including inside attachments if you need it. + +## What should you plan for when you run one? + +Back up the database or files along with the attachments. If the wiki holds internal docs, keep it behind a login or a VPN. For a public docs site, a static site generator may suit you better. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/content/wikis/intro.mdx b/app/(site)/(directories)/self-host-apps/categories/[category]/content/wikis/intro.mdx index 4fbe3cbee..0edf13aa5 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/content/wikis/intro.mdx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/content/wikis/intro.mdx @@ -1 +1 @@ -Need a Confluence alternative for your team's docs? This list has team wikis such as Outline and Docmost, classic wikis such as Wiki.js and DokuWiki, and Gollum for a wiki stored in Git. Each row shows the license, whether the app is still maintained, Docker support, and the minimum RAM when we know it. +Need a Confluence alternative for your team's docs? This list has team wikis such as Outline and Docmost, classic wikis such as Wiki.js and DokuWiki, and Gollum for a wiki stored in Git. Each card shows the source model, pricing, estimated RAM and storage, and the last update. Use the filters to narrow the list. diff --git a/app/(site)/(directories)/self-host-apps/categories/[category]/page.tsx b/app/(site)/(directories)/self-host-apps/categories/[category]/page.tsx index a6c1473e6..c7d63fbf6 100644 --- a/app/(site)/(directories)/self-host-apps/categories/[category]/page.tsx +++ b/app/(site)/(directories)/self-host-apps/categories/[category]/page.tsx @@ -1,29 +1,96 @@ import { notFound } from "next/navigation"; +import { FaGithub } from "react-icons/fa6"; import { getRequiredDirectoryPage } from "@/lib/directory-pages"; -import { getAppCategoryLinks, getSelfHostCategoryApps } from "@/lib/app-categories"; +import { getAppCategoryLinks } from "@/lib/app-categories"; import { appCategoryStaticParams, MIN_CATEGORY_APPS } from "@/lib/app-category-model"; -import { AppCategoryPage } from "@/app/ui/directories/app-category-page"; -import { categoryIntros } from "./content"; +import { getBreadcrumbs } from "@/lib/breadcrumbs"; +import { getSelfHostAppsSearch } from "@/lib/self-host-apps-search-data"; +import { parseSelfHostAppsSearchParams } from "@/lib/self-host-apps-search"; +import { AppCategoryLinks } from "@/app/ui/directories/app-category-links"; +import { DirectoryPageTitle } from "@/app/ui/directories/directory-page-content"; +import { DirectoryQueryProvider } from "@/app/ui/directories/directory-query"; +import { HiddenDetails } from "@/app/ui/directories/hidden-details"; +import { AppShell, SidebarLabel } from "@/app/ui/site/app-shell"; +import { Doodle } from "@/app/ui/site/doodle"; +import { EntryHero } from "@/app/ui/site/entry-hero"; +import { PageCanvas } from "@/app/ui/site/page-canvas"; +import { PageHeader } from "@/app/ui/site/page-header"; +import { PageMeta } from "@/app/ui/site/page-meta"; +import { proseClassName, proseSectionFlowClassName } from "@/app/ui/site/prose"; +import { sidebarPanelGithubLinkClassName, sidebarPanelHeadingClassName } from "@/app/ui/site/sidebar-panel"; +import { siteDoodleSparkArt } from "../../../../art/site--doodle-spark"; +import { siteSpotSelfHostAppsArt } from "../../../../art/site--spot-self-host-apps"; +import { AppDetailsBody } from "../../app-details"; +import { SelfHostAppFilters } from "../../filters"; +import { SelfHostAppResults } from "../../results"; +import { SelfHostAppsSearchProvider } from "../../search"; +import { categoryAbouts, categoryIntros } from "./content"; export const revalidate = false; export async function generateStaticParams() { return appCategoryStaticParams(await getAppCategoryLinks("self-host-apps")); } +// The static page is the category's default view: most popular first, page 1. +// Filters, search, sort, and later pages load from the search API, always with +// this category, and the URL stays on this page. export default async function CategoryPage({ params }: { params: Promise<{ category: string }> }) { const { category } = await params; + const route = `/self-host-apps/categories/${category}`; const directoryPage = await getRequiredDirectoryPage(`self-host-apps/categories/${category}`); const categories = await getAppCategoryLinks("self-host-apps"); - if (!categories.some(entry => entry.slug === category)) notFound(); + const current = categories.find(entry => entry.slug === category); + if (!current) notFound(); const Intro = categoryIntros[category]; if (!Intro) throw new Error(`Missing self-host category intro: ${category}`); - const apps = await getSelfHostCategoryApps(category); - if (apps.length < MIN_CATEGORY_APPS) notFound(); - return } categories={categories} apps={apps.map(app => ({ - name: app.name, summary: app.summary, href: app.href, - facts: [app.license || app.source_model.replaceAll("_", " "), app.maintenance_status, - `Docker: ${app.docker_support.replaceAll("_", " ")}`, - ...(app.ram_min_mb !== null ? [`Minimum RAM: ${app.ram_min_mb} MB`] : [])].join(" · "), - }))} />; + const About = categoryAbouts[category]; + if (!About) throw new Error(`Missing self-host category about section: ${category}`); + const { detailApps, ...initial } = await getSelfHostAppsSearch(parseSelfHostAppsSearchParams(new URLSearchParams({ category }))); + if (initial.total < MIN_CATEGORY_APPS) notFound(); + + const heading = ( +
+ {current.name} + + +
+ ); + + return ( + + + } + sidebarLabel={`${current.name} app filters`} + > + + +

+ + +
+
+ + } /> + ({ slug: app.slug, name: app.name, body: }))} /> + +
+
+ +
+
+
+
+
+
+ ); } diff --git a/app/(site)/(directories)/self-host-apps/filters.tsx b/app/(site)/(directories)/self-host-apps/filters.tsx index f0c9d6ccc..4b7ddfec7 100644 --- a/app/(site)/(directories)/self-host-apps/filters.tsx +++ b/app/(site)/(directories)/self-host-apps/filters.tsx @@ -19,7 +19,6 @@ import { dockerOptions, maintenanceOptions, ramOptions, - route, signalOptions, sortOptions, sourceOptions, @@ -108,8 +107,7 @@ function StorageSlider({ // and the search provider loads the matching apps. export function SelfHostAppFilters({ heading }: { heading: ReactNode }) { const { formKey } = useDirectoryQuery(); - const { params, results } = useSelfHostAppsSearch(); - const { facets } = results; + const { category, facets, params, route } = useSelfHostAppsSearch(); return ( - + {category ? null : ( + + )} = [ ]; export function ActiveFilters() { - const { params, results: { facets } } = useSelfHostAppsSearch(); + const { facets, params, route } = useSelfHostAppsSearch(); const optionLabels = (options: FilterOption[]) => new Map(options.map((option) => [option.value, option.label])); const valueLabels = new Map([ ["category", optionLabels(facets.categories)], diff --git a/app/(site)/(directories)/self-host-apps/options.ts b/app/(site)/(directories)/self-host-apps/options.ts index 371a038b0..a5f5aa31e 100644 --- a/app/(site)/(directories)/self-host-apps/options.ts +++ b/app/(site)/(directories)/self-host-apps/options.ts @@ -2,6 +2,7 @@ import type { DirectoryFilterOption as FilterOption } from "@/app/ui/directories import type { DirectoryParams } from "@/app/ui/directories/directory-params"; import type { SelfHostAppsSearchFacets } from "@/lib/self-host-apps-search"; +/** The hub's path. A category page passes its own path through the search provider. */ export const route = "/self-host-apps"; export const dockerOptions: FilterOption[] = [ @@ -56,10 +57,10 @@ export const defaultSort = "popular"; const values = (options: FilterOption[]) => options.map((option) => option.value); // The keys the search API accepts, in a fixed order so equal views share one cached response. -function allowedValues(facets: SelfHostAppsSearchFacets): Array<[key: string, allowed: string[], multiple: boolean]> { +function allowedValues(facets: SelfHostAppsSearchFacets, lockedCategory: boolean): Array<[key: string, allowed: string[], multiple: boolean]> { return [ ["sort", values(sortOptions), false], - ["category", values(facets.categories), true], + ...(lockedCategory ? [] : [["category", values(facets.categories), true] as [string, string[], boolean]]), ["license", values(facets.licenses), true], ["source", values(sourceOptions), true], ["docker", values(dockerOptions), true], @@ -80,13 +81,14 @@ function list(params: DirectoryParams, key: string) { * The URL view as a clean set of parameters. Unknown keys, tracking tags, and * values that are no longer offered are dropped, as the server page used to do, * so old shared links still open. Empty when the URL shows the default view. + * A category page locks its category, so a `category` key in its URL is dropped. */ -export function cleanParams(params: DirectoryParams, facets: SelfHostAppsSearchFacets): DirectoryParams { +export function cleanParams(params: DirectoryParams, facets: SelfHostAppsSearchFacets, lockedCategory?: string): DirectoryParams { const clean: DirectoryParams = {}; const q = list(params, "q")[0]?.replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 200); if (q) clean.q = q; - for (const [key, allowed, multiple] of allowedValues(facets)) { + for (const [key, allowed, multiple] of allowedValues(facets, Boolean(lockedCategory))) { const picked = [...new Set(list(params, key).filter((value) => allowed.includes(value)))].sort(); if (key === "sort" && picked[0] === defaultSort) continue; if (picked.length === 0) continue; diff --git a/app/(site)/(directories)/self-host-apps/page.tsx b/app/(site)/(directories)/self-host-apps/page.tsx index 2661d48d6..8735c2148 100644 --- a/app/(site)/(directories)/self-host-apps/page.tsx +++ b/app/(site)/(directories)/self-host-apps/page.tsx @@ -63,7 +63,7 @@ export default async function SelfHostAppsPage() { return ( - + show all apps; let content; diff --git a/app/(site)/(directories)/self-host-apps/search.tsx b/app/(site)/(directories)/self-host-apps/search.tsx index e00687e8c..e5e0d596f 100644 --- a/app/(site)/(directories)/self-host-apps/search.tsx +++ b/app/(site)/(directories)/self-host-apps/search.tsx @@ -11,7 +11,13 @@ import { cleanParams, searchQuery } from "./options"; export type SelfHostAppsResults = Omit & { detailApps?: DirectoryApp[] }; type SearchState = { + /** The page path: the hub, or a category page. Links and the form stay on it. */ + route: string; + /** The locked category on a category page. Every request sends it. */ + category?: string; params: DirectoryParams; + /** Sidebar options: the static page's options plus any new ones from loaded results. */ + facets: Facets; results: SelfHostAppsResults; status: "ready" | "loading" | "error" | "invalid"; retry: () => void; @@ -44,15 +50,25 @@ function mergeFacets(base: Facets, latest: Facets): Facets { // The static page shows the default view. Any filter, search, sort, or page in // the URL loads matching apps from the cached search API. Older results stay // visible while the next ones load, and a newer request cancels an older one. -export function SelfHostAppsSearchProvider({ children, initial }: { children: ReactNode; initial: SelfHostAppsResults }) { +// A category page passes its path and category: its static page is that +// category's default view, and every request asks only for that category. +export function SelfHostAppsSearchProvider({ category, children, initial, route }: { + category?: string; + children: ReactNode; + initial: SelfHostAppsResults; + route: string; +}) { const { params: urlParams } = useDirectoryQuery(); const [attempt, setAttempt] = useState(0); const [outcome, setOutcome] = useState(null); const [lastResults, setLastResults] = useState(initial); // Filtered results can offer values the default view does not, so accept both. const facets = useMemo(() => mergeFacets(initial.facets, lastResults.facets), [initial.facets, lastResults.facets]); - const params = useMemo(() => cleanParams(urlParams, facets), [urlParams, facets]); + const params = useMemo(() => cleanParams(urlParams, facets, category), [urlParams, facets, category]); const query = searchQuery(params); + // Same key order as the main page sends (q, sort, category, then the rest), so equal views share one cached response. + const { q, sort, ...rest } = params; + const requestQuery = category && query ? searchQuery({ ...(q ? { q } : {}), ...(sort ? { sort } : {}), category: [category], ...rest }) : query; useEffect(() => { if (!query) return; @@ -63,7 +79,7 @@ export function SelfHostAppsSearchProvider({ children, initial }: { children: Re if ("results" in next) setLastResults(next.results); }; - fetch(`/api/self-host-apps/search?${query}`, { signal: controller.signal }) + fetch(`/api/self-host-apps/search?${requestQuery}`, { signal: controller.signal }) .then(async (response) => { if (response.status === 400) return finish({ attempt, failure: "invalid", query }); if (!response.ok) throw new Error(`Search failed with ${response.status}.`); @@ -72,16 +88,17 @@ export function SelfHostAppsSearchProvider({ children, initial }: { children: Re .catch(() => finish({ attempt, failure: "error", query })); return () => controller.abort(); - }, [attempt, query]); + }, [attempt, query, requestQuery]); const value = useMemo(() => { const retry = () => setAttempt((current) => current + 1); - if (!query) return { params, results: initial, retry, status: "ready" }; + const base = { category, facets, params, retry, route }; + if (!query) return { ...base, results: initial, status: "ready" }; const current = outcome?.query === query && outcome.attempt === attempt ? outcome : null; - if (!current) return { params, results: lastResults, retry, status: "loading" }; - if ("failure" in current) return { params, results: lastResults, retry, status: current.failure }; - return { params, results: current.results, retry, status: "ready" }; - }, [attempt, initial, lastResults, outcome, params, query]); + if (!current) return { ...base, results: lastResults, status: "loading" }; + if ("failure" in current) return { ...base, results: lastResults, status: current.failure }; + return { ...base, results: current.results, status: "ready" }; + }, [attempt, category, facets, initial, lastResults, outcome, params, query, route]); return {children}; } diff --git a/app/ui/directories/app-category-links.module.css b/app/ui/directories/app-category-links.module.css new file mode 100644 index 000000000..67371afa9 --- /dev/null +++ b/app/ui/directories/app-category-links.module.css @@ -0,0 +1,4 @@ +.categories h2 { margin-bottom: var(--space-4); } +.categories ul { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); gap: var(--space-3) var(--space-6); padding: 0; list-style: none; } +.categories span { font-family: var(--font-mono); font-size: var(--text-small); color: var(--color-text-muted); } +.categories strong { font-weight: var(--weight-bold); } diff --git a/app/ui/directories/app-category-links.tsx b/app/ui/directories/app-category-links.tsx index 28ffe7205..aad94c855 100644 --- a/app/ui/directories/app-category-links.tsx +++ b/app/ui/directories/app-category-links.tsx @@ -1,13 +1,16 @@ import type { AppCategoryLink } from "@/lib/app-category-model"; -import styles from "./app-category.module.css"; +import styles from "./app-category-links.module.css"; +/** Every published category of a directory. On a category page, the current one is marked, not linked. */ export function AppCategoryLinks({ categories, current }: { categories: readonly AppCategoryLink[]; current?: string }) { - const links = categories.filter(category => category.slug !== current); - if (!links.length) return null; + if (!categories.length) return null; return ; } diff --git a/app/ui/directories/app-category-page.tsx b/app/ui/directories/app-category-page.tsx deleted file mode 100644 index 3953a77e3..000000000 --- a/app/ui/directories/app-category-page.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import type { ReactNode } from "react"; -import { AppShell } from "@/app/ui/site/app-shell"; -import { PageCanvas } from "@/app/ui/site/page-canvas"; -import { PageHeader } from "@/app/ui/site/page-header"; -import { PageMeta } from "@/app/ui/site/page-meta"; -import { DirectoryPageTitle } from "./directory-page-content"; -import type { Stack127DirectoryPage } from "@/lib/directory-pages"; -import type { AppCategoryLink, AppDirectory } from "@/lib/app-category-model"; -import { getBreadcrumbs } from "@/lib/breadcrumbs"; -import { AppCategoryLinks } from "./app-category-links"; -import { categoryListHtml, type CategoryAppRow } from "@/lib/app-category-html"; -import styles from "./app-category.module.css"; - -export async function AppCategoryPage({ directory, category, directoryPage, intro, apps, categories }: { - directory: AppDirectory; category: string; directoryPage: Stack127DirectoryPage; - intro: ReactNode; apps: readonly CategoryAppRow[]; categories: readonly AppCategoryLink[]; -}) { - return - - -

- - {intro} -
-
-

{apps.length.toLocaleString("en-US")} apps

- {/* Compact HTML avoids a separate React element tree for every row. React - still includes this HTML string in its navigation payload, so the list - travels twice in the initial document. The serializer escapes dataset - text and validates outbound protocols. */} -
    -
- -

Back to {directory === "cli-apps" ? "CLI apps" : "self-host apps"}

-
-
; -} diff --git a/app/ui/directories/app-category.module.css b/app/ui/directories/app-category.module.css deleted file mode 100644 index 5bcc19d16..000000000 --- a/app/ui/directories/app-category.module.css +++ /dev/null @@ -1,10 +0,0 @@ -.categories h2 { margin-bottom: var(--space-4); } -.categories ul { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); gap: var(--space-3) var(--space-6); padding: 0; list-style: none; } -.categories span, .count { font-family: var(--font-mono); font-size: var(--text-small); color: var(--color-text-muted); } -.listing { max-width: 1000px; } -.listing ul { padding: 0; list-style: none; } -.listing li { padding-block: var(--space-4); border-bottom: 1px solid var(--color-border); overflow-wrap: anywhere; } -.listing li:first-child { border-top: 1px solid var(--color-border); } -.listing li > a, .listing li > strong { font-family: var(--font-display); font-weight: var(--weight-bold); } -.listing p { margin-block: var(--space-2); } -.listing small { display: block; font-family: var(--font-mono); font-size: var(--text-small); color: var(--color-text-muted); } diff --git a/lib/app-categories.ts b/lib/app-categories.ts index 8dd65363c..015625acb 100644 --- a/lib/app-categories.ts +++ b/lib/app-categories.ts @@ -3,7 +3,7 @@ import { cache } from "react"; import { getDirectoryAppIndex } from "./apps"; import { getTerminalApps, terminalAppCategories } from "./terminal-apps"; import { getPool } from "./db"; -import { HOME_PAGES_TAG, SELF_HOST_INDEX_TAG } from "./cache-tags"; +import { HOME_PAGES_TAG } from "./cache-tags"; import { countAppCategories, publishedAppCategories, type AppDirectory } from "./app-category-model"; // Every page event invalidates HOME_PAGES_TAG, including a child's publication change. @@ -24,22 +24,3 @@ export const getAppCategoryLinks = cache(async (directory: AppDirectory) => { .sort((a, b) => a.name.localeCompare(b.name)); return publishedAppCategories(directory, countAppCategories(apps, categories, app => app.categories.map(category => category.slug)), pages); }); - -export type SelfHostCategoryApp = { - name: string; summary: string; href: string | null; license: string | null; - source_model: string; maintenance_status: string; docker_support: string; ram_min_mb: number | null; -}; - -// No details, images, hidden panels, or client props. One lean cached query per category. -export const getSelfHostCategoryApps = unstable_cache(async (slug: string): Promise => { - const result = await getPool().query(`select app.name,app.summary,app.popularity_score::float8 as popularity_score, - coalesce(app.source_code_url,app.website_url) as href,coalesce(app.license_spdx_id,app.license) as license, - app.source_model,app.maintenance_status,app.docker_support,app.ram_min_mb - from public.self_host_apps app - where app.published and app.self_hosting_status='self_hostable' and exists ( - select 1 from public.self_host_app_category_links link join public.self_host_app_categories category on category.id=link.category_id - where link.app_id=app.id and category.slug=$1) - order by app.popularity_score desc, app.name asc`, [slug]); - // Match the hub's locale-aware name tie-break, rather than database collation. - return result.rows.sort((a, b) => b.popularity_score - a.popularity_score || a.name.localeCompare(b.name)); -}, ["self-host-category-apps"], { revalidate: false, tags: [SELF_HOST_INDEX_TAG] }); diff --git a/lib/app-category-html.ts b/lib/app-category-html.ts deleted file mode 100644 index 3243feac5..000000000 --- a/lib/app-category-html.ts +++ /dev/null @@ -1,18 +0,0 @@ -export type CategoryAppRow = { name: string; summary: string; href: string | null; facts: string }; - -const escapeHtml = (value: string) => value.replace(/[&<>"']/g, character => ({ - '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', -})[character]!); - -/** All fields are untrusted dataset text. Accept only absolute HTTP(S) outbound URLs. */ -export function categoryListHtml(apps: readonly CategoryAppRow[]) { - return apps.map(app => { - let href = null; - try { - const url = new URL(app.href ?? ''); - if (url.protocol === 'https:' || url.protocol === 'http:') href = url.href; - } catch { /* Missing or malformed links keep the app name as text. */ } - const name = escapeHtml(app.name); - return `
  • ${href ? `${name}` : `${name}`}

    ${escapeHtml(app.summary)}

    ${escapeHtml(app.facts)}
  • `; - }).join(''); -} diff --git a/lib/app-category-model.ts b/lib/app-category-model.ts index 2363a37b2..aba31443b 100644 --- a/lib/app-category-model.ts +++ b/lib/app-category-model.ts @@ -1,4 +1,4 @@ -/** Category pages list the complete default view; query filters stay on the hubs. */ +/** A category page needs at least this many published apps. */ export const MIN_CATEGORY_APPS = 15; export type AppDirectory = "self-host-apps" | "cli-apps"; export type AppCategoryCount = { slug: string; name: string; count: number }; @@ -20,7 +20,3 @@ export function publishedAppCategories(directory: AppDirectory, categories: read export function appCategoryStaticParams(categories: readonly AppCategoryLink[]) { return categories.map(category => ({ category: category.slug })); } - -export function selectCategoryApps(apps: readonly T[], category: string, slugs: (app: T) => readonly string[], score: (app: T) => number): T[] { - return apps.filter(app => slugs(app).includes(category)).sort((a, b) => score(b) - score(a) || a.name.localeCompare(b.name)); -} diff --git a/lib/self-host-apps-search-data.ts b/lib/self-host-apps-search-data.ts index c927f7ff9..b5f23085e 100644 --- a/lib/self-host-apps-search-data.ts +++ b/lib/self-host-apps-search-data.ts @@ -21,7 +21,7 @@ async function search( if (!detail) throw new Error(`Missing search detail: ${app.slug}`); return detail; }); - return { ...results, detailApps, facets: selfHostAppsFacets(index) }; + return { ...results, detailApps, facets: selfHostAppsFacets(index, params.category) }; } export const getSelfHostAppsSearch = (params: SelfHostAppsSearchParams) => diff --git a/lib/self-host-apps-search.ts b/lib/self-host-apps-search.ts index 5475783ec..8280367b1 100644 --- a/lib/self-host-apps-search.ts +++ b/lib/self-host-apps-search.ts @@ -115,7 +115,16 @@ function compareTier(order: string[], left: string, right: string) { return a < 0 ? b < 0 ? 0 : 1 : b < 0 ? -1 : b - a; } -export function selfHostAppsFacets(apps: DirectoryAppIndex[]): SelfHostAppsSearchFacets { +/** + * Sidebar options for the apps in scope: the whole directory, or the selected + * categories. A category page always sends its own category, so its license and + * install options list only what that category has. The hub merges these with + * its full default options, so its sidebar does not shrink. + */ +export function selfHostAppsFacets(index: DirectoryAppIndex[], categoryScope: readonly string[] = []): SelfHostAppsSearchFacets { + const apps = categoryScope.length + ? index.filter(app => app.categories.some(category => categoryScope.includes(category.slug))) + : index; const categories = new Map(); const licenseCounts = new Map(); const installCounts = new Map(); diff --git a/package.json b/package.json index 74633fddb..c6d38bf4c 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,8 @@ "stack127:media:migrate-directory-icons": "node scripts/stack127/media/migrate-directory-icons.mjs", "stack127:media:migrate-json-images": "node scripts/stack127/media/migrate-json-images.mjs", "stack127:repo:dataset:export": "node scripts/stack127/repos/export-dataset.mjs", + "stack127:repo:awesome-list:sync": "node scripts/stack127/repos/sync-awesome-list.mjs", + "stack127:repo:awesome-list:export": "node scripts/stack127/repos/export-awesome-list.mjs", "stack127:repo:dataset:sync": "node scripts/stack127/repos/sync-dataset-repo.mjs", "stack127:repo:tool:new": "node scripts/stack127/repos/create-tool-repo.mjs", "stack127:tools:publish": "node scripts/stack127/tool-pages/publish.mjs", diff --git a/scripts/self-host-apps/apply-artifact-cleanup.mjs b/scripts/self-host-apps/apply-artifact-cleanup.mjs index f91eb4dcb..20bbc4885 100644 --- a/scripts/self-host-apps/apply-artifact-cleanup.mjs +++ b/scripts/self-host-apps/apply-artifact-cleanup.mjs @@ -31,8 +31,8 @@ function identityMatches(row, expected) { return row.slug === expected.slug && row.name === expected.name && row.source_code_url === expected.source_code_url; } -function targetSources(row) { - return [row.source_code_url, row.website_url, row.docs_url, row.install_docs_url, row.docker_url] +export function targetSources(row) { + return [row.source_code_url, row.website_url, row.docs_url, row.install_docs_url, row.docker_url, row.compose_url, ...(row.data_source_urls ?? [])] .filter((value) => typeof value === "string"); } @@ -97,6 +97,9 @@ function validateIdentities(manifest, targetRows, canonicalRows) { if (!row || row.slug !== spec.slug || !spec.expected_names.includes(row.name) || row.source_code_url !== spec.source_code_url) { throw new Error(`Cleanup stopped: canonical identity mismatch for ${spec.slug}.`); } + if (!row.published || row.self_hosting_status !== "self_hostable") { + throw new Error(`Cleanup stopped: canonical app ${spec.slug} is not published and self-hostable.`); + } } return states; } @@ -130,11 +133,27 @@ async function applyCleanup(client, manifest, targetRows, canonicalRows) { ); } +/** Fails before any database work when the file contradicts itself. */ +export function validateManifest(manifest) { + const targets = new Set(manifest.records.map((row) => row.slug)); + if (targets.size !== manifest.records.length || manifest.canonical_apps.some((row) => targets.has(row.slug))) { + throw new Error("Cleanup stopped: repeated target or canonical app is also a retirement target."); + } + const canonicals = manifest.canonical_apps.map((row) => row.slug); + if (new Set(canonicals).size !== canonicals.length) throw new Error("Cleanup stopped: a canonical app is listed twice."); + for (const record of manifest.records) { + if (record.canonical_slug && !canonicals.includes(record.canonical_slug)) { + throw new Error(`Cleanup stopped: ${record.slug} points to unknown canonical app ${record.canonical_slug}.`); + } + } +} + async function main() { const args = parseArgs(); const { environment, apply, databaseUrl } = cleanupOptions(args); - const manifest = JSON.parse(await readFile(manifestPath, "utf8")); + const manifest = JSON.parse(await readFile(args.file ?? manifestPath, "utf8")); + validateManifest(manifest); await withClient(async (client) => { await client.query(apply ? "begin isolation level serializable" : "begin read only"); try { diff --git a/scripts/self-host-apps/batches/2026-11-13-published-duplicates-reviewed.json b/scripts/self-host-apps/batches/2026-11-13-published-duplicates-reviewed.json new file mode 100644 index 000000000..cb6671bf9 --- /dev/null +++ b/scripts/self-host-apps/batches/2026-11-13-published-duplicates-reviewed.json @@ -0,0 +1,6892 @@ +{ + "reviewed_at": "2026-11-13", + "scope": "Production published self_hostable apps; every shared repository and normalized-name/website collision reviewed. No automatic retirement by repository.", + "merge_policy": "Existing cleanup preserves evidence URLs only. Structured facts are retained on unpublished rows and listed for follow-up review; no blind category/resource/install merges.", + "canonical_apps": [ + { + "slug": "seerr", + "expected_names": [ + "Seerr" + ], + "source_code_url": "https://github.com/seerr-team/seerr" + }, + { + "slug": "jellyfin", + "expected_names": [ + "Jellyfin" + ], + "source_code_url": "https://github.com/jellyfin/jellyfin" + }, + { + "slug": "motion-tools-antragsgr-n", + "expected_names": [ + "motion.tools (Antragsgrün)" + ], + "source_code_url": "https://github.com/CatoTH/antragsgruen" + }, + { + "slug": "uwave", + "expected_names": [ + "uWave" + ], + "source_code_url": "https://github.com/u-wave/core" + }, + { + "slug": "backdrop-cms", + "expected_names": [ + "Backdrop CMS" + ], + "source_code_url": "https://github.com/backdrop/backdrop" + }, + { + "slug": "gladys-assistant", + "expected_names": [ + "Gladys Assistant" + ], + "source_code_url": "https://github.com/gladysassistant/gladys" + }, + { + "slug": "cap", + "expected_names": [ + "Cap" + ], + "source_code_url": "https://github.com/CapSoftware/Cap" + }, + { + "slug": "openg2p-spar-self-service-api", + "expected_names": [ + "Openg2p Spar Self Service Api" + ], + "source_code_url": "https://github.com/OpenG2P/openg2p-spar-self-service" + }, + { + "slug": "invoice-ninja", + "expected_names": [ + "Invoice Ninja" + ], + "source_code_url": "https://github.com/invoiceninja/invoiceninja" + }, + { + "slug": "cryptgeon", + "expected_names": [ + "Cryptgeon" + ], + "source_code_url": "https://github.com/cupcakearmy/cryptgeon" + }, + { + "slug": "13ft", + "expected_names": [ + "13ft" + ], + "source_code_url": "https://github.com/wasi-master/13ft" + }, + { + "slug": "highlight", + "expected_names": [ + "Highlight" + ], + "source_code_url": "https://github.com/highlight/highlight" + }, + { + "slug": "baikal", + "expected_names": [ + "Baikal" + ], + "source_code_url": "https://github.com/sabre-io/Baikal" + }, + { + "slug": "luanti", + "expected_names": [ + "Luanti" + ], + "source_code_url": "https://github.com/luanti-org/luanti" + }, + { + "slug": "bitmagnet", + "expected_names": [ + "bitmagnet" + ], + "source_code_url": "https://github.com/bitmagnet-io/bitmagnet" + }, + { + "slug": "code-server", + "expected_names": [ + "code-server" + ], + "source_code_url": "https://github.com/coder/code-server" + }, + { + "slug": "filebrowser", + "expected_names": [ + "filebrowser" + ], + "source_code_url": "https://github.com/filebrowser/filebrowser" + }, + { + "slug": "flaresolverr", + "expected_names": [ + "FlareSolverr" + ], + "source_code_url": "https://github.com/FlareSolverr/FlareSolverr" + }, + { + "slug": "maybe", + "expected_names": [ + "Maybe" + ], + "source_code_url": "https://github.com/maybe-finance/maybe" + }, + { + "slug": "converse-js", + "expected_names": [ + "Converse.js" + ], + "source_code_url": "https://github.com/conversejs/converse.js" + }, + { + "slug": "navidrome", + "expected_names": [ + "Navidrome" + ], + "source_code_url": "https://github.com/navidrome/navidrome" + }, + { + "slug": "lidarr", + "expected_names": [ + "Lidarr" + ], + "source_code_url": "https://github.com/Lidarr/Lidarr" + }, + { + "slug": "nginx", + "expected_names": [ + "NGINX" + ], + "source_code_url": "https://github.com/nginx/nginx" + }, + { + "slug": "nzbget", + "expected_names": [ + "NZBGet" + ], + "source_code_url": "https://github.com/nzbgetcom/nzbget" + }, + { + "slug": "mirotalk-p2p", + "expected_names": [ + "MiroTalk P2P" + ], + "source_code_url": "https://github.com/miroslavpejic85/mirotalk" + }, + { + "slug": "ddracenetwork", + "expected_names": [ + "DDraceNetwork" + ], + "source_code_url": "https://github.com/ddnet/ddnet" + }, + { + "slug": "rclone", + "expected_names": [ + "Rclone" + ], + "source_code_url": "https://github.com/rclone/rclone" + }, + { + "slug": "slskd", + "expected_names": [ + "slskd" + ], + "source_code_url": "https://github.com/slskd/slskd" + }, + { + "slug": "draw-io", + "expected_names": [ + "draw.io" + ], + "source_code_url": "https://github.com/jgraph/drawio" + }, + { + "slug": "syncthing", + "expected_names": [ + "Syncthing" + ], + "source_code_url": "https://github.com/syncthing/syncthing" + }, + { + "slug": "bitwarden", + "expected_names": [ + "Bitwarden" + ], + "source_code_url": "https://github.com/bitwarden/server" + }, + { + "slug": "gonic", + "expected_names": [ + "gonic" + ], + "source_code_url": "https://github.com/sentriz/gonic" + }, + { + "slug": "jitsi-meet", + "expected_names": [ + "Jitsi Meet" + ], + "source_code_url": "https://github.com/jitsi/jitsi-meet" + }, + { + "slug": "rustdesk", + "expected_names": [ + "RustDesk Server OSS" + ], + "source_code_url": "https://github.com/rustdesk/rustdesk-server" + }, + { + "slug": "changedetection-io", + "expected_names": [ + "changedetection.io" + ], + "source_code_url": "https://github.com/dgtlmoon/changedetection.io" + }, + { + "slug": "wbo", + "expected_names": [ + "WBO" + ], + "source_code_url": "https://github.com/lovasoa/whitebophir" + }, + { + "slug": "readarr", + "expected_names": [ + "Readarr" + ], + "source_code_url": "https://github.com/Readarr/Readarr" + }, + { + "slug": "vvveb-cms", + "expected_names": [ + "Vvveb CMS" + ], + "source_code_url": "https://github.com/givanz/Vvveb" + }, + { + "slug": "wallabag", + "expected_names": [ + "Wallabag" + ], + "source_code_url": "https://github.com/wallabag/wallabag" + }, + { + "slug": "tiny-tiny-rss", + "expected_names": [ + "Tiny Tiny RSS" + ], + "source_code_url": "https://github.com/tt-rss/tt-rss" + }, + { + "slug": "mantisbt", + "expected_names": [ + "MantisBT" + ], + "source_code_url": "https://github.com/mantisbt/mantisbt" + }, + { + "slug": "sftpgo", + "expected_names": [ + "SFTPGo" + ], + "source_code_url": "https://github.com/drakkan/sftpgo" + }, + { + "slug": "docs", + "expected_names": [ + "Docs" + ], + "source_code_url": "https://github.com/suitenumerique/docs" + }, + { + "slug": "kuvasz", + "expected_names": [ + "Kuvasz" + ], + "source_code_url": "https://github.com/kuvasz-uptime/kuvasz" + }, + { + "slug": "umbrelos", + "expected_names": [ + "umbrelOS" + ], + "source_code_url": "https://github.com/getumbrel/umbrel" + }, + { + "slug": "docmost", + "expected_names": [ + "docmost" + ], + "source_code_url": "https://github.com/docmost/docmost" + }, + { + "slug": "wud", + "expected_names": [ + "WUD" + ], + "source_code_url": "https://github.com/getwud/wud" + }, + { + "slug": "comfyui", + "expected_names": [ + "ComfyUI" + ], + "source_code_url": "https://github.com/comfy-org/comfyui" + }, + { + "slug": "dify", + "expected_names": [ + "Dify" + ], + "source_code_url": "https://github.com/langgenius/dify" + }, + { + "slug": "technitium-dns-server", + "expected_names": [ + "Technitium DNS Server" + ], + "source_code_url": "https://github.com/TechnitiumSoftware/DnsServer" + }, + { + "slug": "harness", + "expected_names": [ + "Harness" + ], + "source_code_url": "https://github.com/harness/harness" + }, + { + "slug": "distribution", + "expected_names": [ + "Distribution Registry" + ], + "source_code_url": "https://github.com/distribution/distribution" + }, + { + "slug": "garage-s3-simple", + "expected_names": [ + "garage-s3-simple" + ], + "source_code_url": "https://git.deuxfleurs.fr/Deuxfleurs/garage" + }, + { + "slug": "homebox-sysadminsmedia", + "expected_names": [ + "HomeBox (SysAdminsMedia)" + ], + "source_code_url": "https://github.com/sysadminsmedia/homebox" + }, + { + "slug": "swag-secure-web-application-gateway", + "expected_names": [ + "SWAG (Secure Web Application Gateway)" + ], + "source_code_url": "https://github.com/linuxserver/docker-swag" + }, + { + "slug": "privatebin", + "expected_names": [ + "PrivateBin" + ], + "source_code_url": "https://github.com/PrivateBin/PrivateBin" + }, + { + "slug": "odoo", + "expected_names": [ + "Odoo" + ], + "source_code_url": "https://github.com/odoo/odoo" + }, + { + "slug": "pgadmin4", + "expected_names": [ + "pgAdmin 4" + ], + "source_code_url": "https://github.com/pgadmin-org/pgadmin4" + }, + { + "slug": "tvheadend", + "expected_names": [ + "TVHeadend" + ], + "source_code_url": "https://github.com/tvheadend/tvheadend" + }, + { + "slug": "ketesa", + "expected_names": [ + "Ketesa" + ], + "source_code_url": "https://github.com/etkecc/ketesa" + }, + { + "slug": "woodpecker-ci", + "expected_names": [ + "Woodpecker CI" + ], + "source_code_url": "https://github.com/woodpecker-ci/woodpecker" + }, + { + "slug": "kimai", + "expected_names": [ + "Kimai" + ], + "source_code_url": "https://github.com/kimai/kimai" + }, + { + "slug": "homepage", + "expected_names": [ + "Homepage" + ], + "source_code_url": "https://github.com/gethomepage/homepage" + }, + { + "slug": "campfire", + "expected_names": [ + "Campfire" + ], + "source_code_url": "https://github.com/basecamp/once-campfire" + }, + { + "slug": "urbackup", + "expected_names": [ + "UrBackup" + ], + "source_code_url": "https://github.com/uroni/urbackup_backend" + }, + { + "slug": "firefly-iii-data-importer", + "expected_names": [ + "Firefly III Data Importer" + ], + "source_code_url": "https://github.com/firefly-iii/data-importer" + }, + { + "slug": "tandoor-recipes", + "expected_names": [ + "Tandoor Recipes" + ], + "source_code_url": "https://github.com/TandoorRecipes/recipes" + }, + { + "slug": "kiwix-serve", + "expected_names": [ + "kiwix-serve" + ], + "source_code_url": "https://github.com/kiwix/kiwix-tools" + }, + { + "slug": "synapse", + "expected_names": [ + "Synapse" + ], + "source_code_url": "https://github.com/element-hq/synapse" + }, + { + "slug": "triliumnext-notes", + "expected_names": [ + "TriliumNext Notes", + "Trilium Notes" + ], + "clean_name": "Trilium Notes", + "source_code_url": "https://github.com/TriliumNext/Trilium" + }, + { + "slug": "mumble", + "expected_names": [ + "Mumble" + ], + "source_code_url": "https://github.com/mumble-voip/mumble" + }, + { + "slug": "apache-druid", + "expected_names": [ + "Apache Druid" + ], + "source_code_url": "https://github.com/apache/druid" + }, + { + "slug": "standard-notes", + "expected_names": [ + "Standard Notes" + ], + "source_code_url": "https://github.com/standardnotes/app" + }, + { + "slug": "gluetunvpn", + "expected_names": [ + "GluetunVPN" + ], + "source_code_url": "https://github.com/passteque/gluetun" + }, + { + "slug": "shopware-community-edition", + "expected_names": [ + "Shopware Community Edition" + ], + "source_code_url": "https://github.com/shopware/shopware" + }, + { + "slug": "apache-answer", + "expected_names": [ + "Apache Answer" + ], + "source_code_url": "https://github.com/apache/answer" + }, + { + "slug": "mealie", + "expected_names": [ + "Mealie" + ], + "source_code_url": "https://github.com/mealie-recipes/mealie" + }, + { + "slug": "borg-ui", + "expected_names": [ + "Borg UI" + ], + "source_code_url": "https://github.com/karanhudia/borg-ui" + }, + { + "slug": "handbrake", + "expected_names": [ + "HandBrake" + ], + "source_code_url": "https://github.com/HandBrake/HandBrake" + }, + { + "slug": "jenkins", + "expected_names": [ + "Jenkins" + ], + "source_code_url": "https://github.com/jenkinsci/jenkins" + }, + { + "slug": "lobehub", + "expected_names": [ + "LobeHub" + ], + "source_code_url": "https://github.com/lobehub/lobehub" + }, + { + "slug": "phpservermonitor", + "expected_names": [ + "PHPServerMonitor" + ], + "source_code_url": "https://github.com/phpservermon/phpservermon" + }, + { + "slug": "sim-studio", + "expected_names": [ + "Sim Studio" + ], + "source_code_url": "https://github.com/simstudioai/sim" + }, + { + "slug": "snipe-it", + "expected_names": [ + "Snipe-IT" + ], + "source_code_url": "https://github.com/grokability/snipe-it" + }, + { + "slug": "foundry-virtual-tabletop", + "expected_names": [ + "Foundry Virtual Tabletop" + ], + "source_code_url": "https://github.com/foundryvtt/foundryvtt" + }, + { + "slug": "tiny-media-manager", + "expected_names": [ + "Tiny Media Manager" + ], + "source_code_url": null + }, + { + "slug": "musicbrainz-picard", + "expected_names": [ + "MusicBrainz Picard" + ], + "source_code_url": "https://github.com/metabrainz/picard" + }, + { + "slug": "sqlite-browser", + "expected_names": [ + "SQLite Browser" + ], + "source_code_url": "https://github.com/sqlitebrowser/sqlitebrowser" + }, + { + "slug": "one-time-secret", + "expected_names": [ + "One Time Secret" + ], + "source_code_url": "https://github.com/onetimesecret/onetimesecret" + }, + { + "slug": "jan-ai", + "expected_names": [ + "Jan Ai" + ], + "source_code_url": "https://github.com/menloresearch/jan" + }, + { + "slug": "nextcloud", + "expected_names": [ + "Nextcloud" + ], + "source_code_url": "https://github.com/nextcloud/server" + }, + { + "slug": "sentry", + "expected_names": [ + "Sentry" + ], + "source_code_url": "https://github.com/getsentry/sentry" + }, + { + "slug": "collabora-code", + "expected_names": [ + "Collabora-CODE" + ], + "source_code_url": "https://github.com/CollaboraOnline/online.mirror" + } + ], + "records": [ + { + "slug": "binhex-seerr", + "kind": "duplicate", + "name": "binhex-seerr", + "source_code_url": "https://github.com/seerr-team/seerr", + "canonical_slug": "seerr" + }, + { + "slug": "jellyseerr", + "kind": "duplicate", + "name": "Jellyseerr", + "source_code_url": "https://github.com/seerr-team/seerr", + "canonical_slug": "seerr" + }, + { + "slug": "binhex-jellyfin", + "kind": "duplicate", + "name": "binhex-jellyfin", + "source_code_url": "https://github.com/jellyfin/jellyfin", + "canonical_slug": "jellyfin" + }, + { + "slug": "motion-tools-antragsgrun", + "kind": "duplicate", + "name": "Motion Tools Antragsgrun", + "source_code_url": "https://github.com/CatoTH/antragsgruen", + "canonical_slug": "motion-tools-antragsgr-n" + }, + { + "slug": "wave", + "kind": "duplicate", + "name": "üWave", + "source_code_url": "https://github.com/u-wave/core", + "canonical_slug": "uwave" + }, + { + "slug": "backdrop", + "kind": "duplicate", + "name": "Backdrop", + "source_code_url": "https://github.com/backdrop/backdrop", + "canonical_slug": "backdrop-cms" + }, + { + "slug": "gladys", + "kind": "duplicate", + "name": "Gladys", + "source_code_url": "https://github.com/GladysAssistant/Gladys", + "canonical_slug": "gladys-assistant" + }, + { + "slug": "cap-so", + "kind": "duplicate", + "name": "Cap So", + "source_code_url": "https://github.com/CapSoftware/Cap", + "canonical_slug": "cap" + }, + { + "slug": "openg2p-selfservice-api", + "kind": "duplicate", + "name": "Openg2p Selfservice Api", + "source_code_url": "https://github.com/OpenG2P/openg2p-spar-self-service", + "canonical_slug": "openg2p-spar-self-service-api" + }, + { + "slug": "invoiceninja5", + "kind": "duplicate", + "name": "Invoiceninja5", + "source_code_url": "https://github.com/invoiceninja/invoiceninja", + "canonical_slug": "invoice-ninja" + }, + { + "slug": "cryptgeon-redis", + "kind": "duplicate", + "name": "cryptgeon_redis", + "source_code_url": "https://github.com/cupcakearmy/cryptgeon", + "canonical_slug": "cryptgeon" + }, + { + "slug": "13ft-ladder", + "kind": "duplicate", + "name": "13ft Ladder", + "source_code_url": "https://github.com/wasi-master/13ft", + "canonical_slug": "13ft" + }, + { + "slug": "highlight-io", + "kind": "duplicate", + "name": "Highlight Io", + "source_code_url": "https://github.com/highlight/highlight", + "canonical_slug": "highlight" + }, + { + "slug": "ba-kal", + "kind": "duplicate", + "name": "Baïkal", + "source_code_url": "https://github.com/sabre-io/Baikal", + "canonical_slug": "baikal" + }, + { + "slug": "minetest", + "kind": "duplicate", + "name": "Minetest", + "source_code_url": "https://github.com/luanti-org/luanti", + "canonical_slug": "luanti" + }, + { + "slug": "binhex-bitmagnet", + "kind": "duplicate", + "name": "binhex-bitmagnet", + "source_code_url": "https://github.com/bitmagnet-io/bitmagnet", + "canonical_slug": "bitmagnet" + }, + { + "slug": "binhex-code-server", + "kind": "duplicate", + "name": "binhex-code-server", + "source_code_url": "https://github.com/coder/code-server", + "canonical_slug": "code-server" + }, + { + "slug": "binhex-filebrowser", + "kind": "duplicate", + "name": "binhex-filebrowser", + "source_code_url": "https://github.com/filebrowser/filebrowser", + "canonical_slug": "filebrowser" + }, + { + "slug": "binhex-flaresolverr", + "kind": "duplicate", + "name": "binhex-flaresolverr", + "source_code_url": "https://github.com/FlareSolverr/FlareSolverr", + "canonical_slug": "flaresolverr" + }, + { + "slug": "maybe-web", + "kind": "duplicate", + "name": "Maybe_Web", + "source_code_url": "https://github.com/maybe-finance/maybe", + "canonical_slug": "maybe" + }, + { + "slug": "converse", + "kind": "duplicate", + "name": "Converse", + "source_code_url": "https://github.com/conversejs/converse.js", + "canonical_slug": "converse-js" + }, + { + "slug": "navidrome-music-server", + "kind": "duplicate", + "name": "Navidrome Music Server", + "source_code_url": "https://github.com/navidrome/navidrome", + "canonical_slug": "navidrome" + }, + { + "slug": "binhex-lidarr", + "kind": "duplicate", + "name": "binhex-lidarr", + "source_code_url": "https://github.com/Lidarr/Lidarr", + "canonical_slug": "lidarr" + }, + { + "slug": "binhex-nginx", + "kind": "duplicate", + "name": "binhex-nginx", + "source_code_url": "https://github.com/nginx/nginx", + "canonical_slug": "nginx" + }, + { + "slug": "binhex-nzbget", + "kind": "duplicate", + "name": "binhex-nzbget", + "source_code_url": "https://github.com/nzbgetcom/nzbget", + "canonical_slug": "nzbget" + }, + { + "slug": "mirotalk", + "kind": "duplicate", + "name": "Mirotalk", + "source_code_url": "https://github.com/miroslavpejic85/mirotalk", + "canonical_slug": "mirotalk-p2p" + }, + { + "slug": "ddnet", + "kind": "duplicate", + "name": "DDNet", + "source_code_url": "https://github.com/ddnet/ddnet", + "canonical_slug": "ddracenetwork" + }, + { + "slug": "binhex-rclone", + "kind": "duplicate", + "name": "binhex-rclone", + "source_code_url": "https://github.com/rclone/rclone", + "canonical_slug": "rclone" + }, + { + "slug": "binhex-slskd", + "kind": "duplicate", + "name": "binhex-slskd", + "source_code_url": "https://github.com/slskd/slskd", + "canonical_slug": "slskd" + }, + { + "slug": "diagramsnet", + "kind": "duplicate", + "name": "Diagramsnet", + "source_code_url": "https://github.com/jgraph/drawio", + "canonical_slug": "draw-io" + }, + { + "slug": "binhex-syncthing", + "kind": "duplicate", + "name": "binhex-syncthing", + "source_code_url": "https://github.com/syncthing/syncthing", + "canonical_slug": "syncthing" + }, + { + "slug": "bitwarden-lite", + "kind": "duplicate", + "name": "Bitwarden Lite", + "source_code_url": "https://github.com/bitwarden/server", + "canonical_slug": "bitwarden" + }, + { + "slug": "binhex-gonic", + "kind": "duplicate", + "name": "binhex-gonic", + "source_code_url": "https://github.com/sentriz/gonic", + "canonical_slug": "gonic" + }, + { + "slug": "jitsi", + "kind": "duplicate", + "name": "Jitsi", + "source_code_url": "https://github.com/jitsi/jitsi-meet", + "canonical_slug": "jitsi-meet" + }, + { + "slug": "rustdesk-server", + "kind": "duplicate", + "name": "Rustdesk Server", + "source_code_url": "https://github.com/rustdesk/rustdesk-server", + "canonical_slug": "rustdesk" + }, + { + "slug": "changedetection", + "kind": "duplicate", + "name": "Changedetection", + "source_code_url": "https://github.com/dgtlmoon/changedetection.io", + "canonical_slug": "changedetection-io" + }, + { + "slug": "whitebophir", + "kind": "duplicate", + "name": "Whitebophir", + "source_code_url": "https://github.com/lovasoa/whitebophir", + "canonical_slug": "wbo" + }, + { + "slug": "binhex-readarr", + "kind": "duplicate", + "name": "binhex-readarr", + "source_code_url": "https://github.com/Readarr/Readarr", + "canonical_slug": "readarr" + }, + { + "slug": "vvveb", + "kind": "duplicate", + "name": "Vvveb", + "source_code_url": "https://github.com/givanz/Vvveb", + "canonical_slug": "vvveb-cms" + }, + { + "slug": "wallabag2", + "kind": "duplicate", + "name": "Wallabag2", + "source_code_url": "https://github.com/wallabag/wallabag", + "canonical_slug": "wallabag" + }, + { + "slug": "ttrss", + "kind": "duplicate", + "name": "Ttrss", + "source_code_url": "https://github.com/tt-rss/tt-rss", + "canonical_slug": "tiny-tiny-rss" + }, + { + "slug": "mantis", + "kind": "duplicate", + "name": "Mantis", + "source_code_url": "https://github.com/mantisbt/mantisbt", + "canonical_slug": "mantisbt" + }, + { + "slug": "sftpgo-community-edition", + "kind": "duplicate", + "name": "Sftpgo Community Edition", + "source_code_url": "https://github.com/drakkan/sftpgo", + "canonical_slug": "sftpgo" + }, + { + "slug": "lasuite-docs", + "kind": "duplicate", + "name": "Lasuite Docs", + "source_code_url": "https://github.com/suitenumerique/docs", + "canonical_slug": "docs" + }, + { + "slug": "kuvasz-uptime", + "kind": "duplicate", + "name": "Kuvasz Uptime", + "source_code_url": "https://github.com/kuvasz-uptime/kuvasz", + "canonical_slug": "kuvasz" + }, + { + "slug": "umbrel", + "kind": "duplicate", + "name": "Umbrel", + "source_code_url": "https://github.com/getumbrel/umbrel", + "canonical_slug": "umbrelos" + }, + { + "slug": "docmost-community-edition", + "kind": "duplicate", + "name": "Docmost Community Edition", + "source_code_url": "https://github.com/docmost/docmost", + "canonical_slug": "docmost" + }, + { + "slug": "whats-up-docker", + "kind": "duplicate", + "name": "What’s Up Docker (WUD)", + "source_code_url": "https://github.com/getwud/wud", + "canonical_slug": "wud" + }, + { + "slug": "comfyui-intel-xpu", + "kind": "duplicate", + "name": "ComfyUI-Intel-XPU", + "source_code_url": "https://github.com/Comfy-Org/ComfyUI", + "canonical_slug": "comfyui" + }, + { + "slug": "dify-ai", + "kind": "duplicate", + "name": "Dify.ai", + "source_code_url": "https://github.com/langgenius/dify", + "canonical_slug": "dify" + }, + { + "slug": "technitium-dns", + "kind": "duplicate", + "name": "Technitium Dns", + "source_code_url": "https://github.com/TechnitiumSoftware/DnsServer", + "canonical_slug": "technitium-dns-server" + }, + { + "slug": "gitness", + "kind": "duplicate", + "name": "Harness Open Source", + "source_code_url": "https://github.com/harness/harness", + "canonical_slug": "harness" + }, + { + "slug": "dockerregistry", + "kind": "duplicate", + "name": "Docker Registry", + "source_code_url": "https://github.com/distribution/distribution", + "canonical_slug": "distribution" + }, + { + "slug": "garage-s3-tailscale", + "kind": "duplicate", + "name": "garage-s3-tailscale", + "source_code_url": "https://git.deuxfleurs.fr/Deuxfleurs/garage", + "canonical_slug": "garage-s3-simple" + }, + { + "slug": "homebox", + "kind": "duplicate", + "name": "Homebox", + "source_code_url": "https://github.com/sysadminsmedia/homebox", + "canonical_slug": "homebox-sysadminsmedia" + }, + { + "slug": "swag", + "kind": "duplicate", + "name": "Swag", + "source_code_url": "https://github.com/linuxserver/docker-swag", + "canonical_slug": "swag-secure-web-application-gateway" + }, + { + "slug": "zerobin", + "kind": "duplicate", + "name": "Zerobin", + "source_code_url": "https://github.com/PrivateBin/PrivateBin", + "canonical_slug": "privatebin" + }, + { + "slug": "odoo17", + "kind": "duplicate", + "name": "Odoo 17 Community", + "source_code_url": "https://github.com/odoo/odoo", + "canonical_slug": "odoo" + }, + { + "slug": "libreerp", + "kind": "duplicate", + "name": "Libreerp", + "source_code_url": "https://github.com/odoo/odoo", + "canonical_slug": "odoo" + }, + { + "slug": "pgadmin", + "kind": "duplicate", + "name": "pgAdmin", + "source_code_url": "https://github.com/pgadmin-org/pgadmin4", + "canonical_slug": "pgadmin4" + }, + { + "slug": "binhex-tvheadend", + "kind": "duplicate", + "name": "binhex-tvheadend", + "source_code_url": "https://github.com/tvheadend/tvheadend", + "canonical_slug": "tvheadend" + }, + { + "slug": "synapse-admin", + "kind": "duplicate", + "name": "Synapse Admin", + "source_code_url": "https://github.com/etkecc/ketesa", + "canonical_slug": "ketesa" + }, + { + "slug": "woodpecker", + "kind": "duplicate", + "name": "Woodpecker", + "source_code_url": "https://github.com/woodpecker-ci/woodpecker", + "canonical_slug": "woodpecker-ci" + }, + { + "slug": "kimai2", + "kind": "duplicate", + "name": "Kimai2", + "source_code_url": "https://github.com/kimai/kimai", + "canonical_slug": "kimai" + }, + { + "slug": "homepage-by-gethomepage", + "kind": "duplicate", + "name": "Homepage by gethomepage", + "source_code_url": "https://github.com/gethomepage/homepage", + "canonical_slug": "homepage" + }, + { + "slug": "once-campfire", + "kind": "duplicate", + "name": "Once Campfire", + "source_code_url": "https://github.com/basecamp/once-campfire", + "canonical_slug": "campfire" + }, + { + "slug": "binhex-urbackup", + "kind": "duplicate", + "name": "binhex-urbackup", + "source_code_url": "https://github.com/uroni/urbackup_backend", + "canonical_slug": "urbackup" + }, + { + "slug": "firefly-iii-di", + "kind": "duplicate", + "name": "Firefly Iii Di", + "source_code_url": "https://github.com/firefly-iii/data-importer", + "canonical_slug": "firefly-iii-data-importer" + }, + { + "slug": "tandoor", + "kind": "duplicate", + "name": "Tandoor", + "source_code_url": "https://github.com/TandoorRecipes/recipes", + "canonical_slug": "tandoor-recipes" + }, + { + "slug": "kiwix", + "kind": "duplicate", + "name": "Kiwix", + "source_code_url": "https://github.com/kiwix/kiwix-tools", + "canonical_slug": "kiwix-serve" + }, + { + "slug": "matrix", + "kind": "duplicate", + "name": "Synapse (Matrix Homeserver)", + "source_code_url": "https://github.com/element-hq/synapse", + "canonical_slug": "synapse" + }, + { + "slug": "matrix-synapse", + "kind": "duplicate", + "name": "Matrix-Synapse", + "source_code_url": "https://github.com/element-hq/synapse", + "canonical_slug": "synapse" + }, + { + "slug": "trilium", + "kind": "duplicate", + "name": "Trilium", + "source_code_url": "https://github.com/TriliumNext/trilium", + "canonical_slug": "triliumnext-notes" + }, + { + "slug": "mumbleserver", + "kind": "duplicate", + "name": "Mumbleserver", + "source_code_url": "https://github.com/mumble-voip/mumble", + "canonical_slug": "mumble" + }, + { + "slug": "druid", + "kind": "duplicate", + "name": "Druid", + "source_code_url": "https://github.com/apache/druid", + "canonical_slug": "apache-druid" + }, + { + "slug": "snweb", + "kind": "duplicate", + "name": "Snweb", + "source_code_url": "https://github.com/standardnotes/app", + "canonical_slug": "standard-notes" + }, + { + "slug": "standardnotes-web", + "kind": "duplicate", + "name": "standardnotes-web", + "source_code_url": "https://github.com/standardnotes/app", + "canonical_slug": "standard-notes" + }, + { + "slug": "gluetun-full", + "kind": "duplicate", + "name": "Gluetun-Full", + "source_code_url": "https://github.com/passteque/gluetun", + "canonical_slug": "gluetunvpn" + }, + { + "slug": "shopware", + "kind": "duplicate", + "name": "Shopware", + "source_code_url": "https://github.com/shopware/shopware", + "canonical_slug": "shopware-community-edition" + }, + { + "slug": "answer", + "kind": "duplicate", + "name": "Answer", + "source_code_url": "https://github.com/apache/answer", + "canonical_slug": "apache-answer" + }, + { + "slug": "mealiev1", + "kind": "duplicate", + "name": "mealiev1", + "source_code_url": "https://github.com/mealie-recipes/mealie", + "canonical_slug": "mealie" + }, + { + "slug": "borg-web-ui", + "kind": "duplicate", + "name": "Borg-Web-UI", + "source_code_url": "https://github.com/karanhudia/borg-ui", + "canonical_slug": "borg-ui" + }, + { + "slug": "handbrake-for-nvidia", + "kind": "duplicate", + "name": "HandBrake-for-Nvidia", + "source_code_url": "https://github.com/HandBrake/HandBrake", + "canonical_slug": "handbrake" + }, + { + "slug": "binhex-jenkins", + "kind": "duplicate", + "name": "binhex-jenkins", + "source_code_url": "https://github.com/jenkinsci/jenkins", + "canonical_slug": "jenkins" + }, + { + "slug": "lobechat", + "kind": "duplicate", + "name": "LobeChat", + "source_code_url": "https://github.com/lobehub/lobehub", + "canonical_slug": "lobehub" + }, + { + "slug": "phpservermon", + "kind": "duplicate", + "name": "Phpservermon", + "source_code_url": "https://github.com/phpservermon/phpservermon", + "canonical_slug": "phpservermonitor" + }, + { + "slug": "simstudio-app", + "kind": "duplicate", + "name": "simstudio-app", + "source_code_url": "https://github.com/simstudioai/sim", + "canonical_slug": "sim-studio" + }, + { + "slug": "snipeit", + "kind": "duplicate", + "name": "Snipeit", + "source_code_url": "https://github.com/snipe/snipe-it", + "canonical_slug": "snipe-it" + }, + { + "slug": "tandoorrecipes", + "kind": "duplicate", + "name": "TandoorRecipes", + "source_code_url": "https://github.com/vabene1111/recipes", + "canonical_slug": "tandoor-recipes" + }, + { + "slug": "foundry", + "kind": "duplicate", + "name": "Foundry Virtual Tabletop", + "source_code_url": null, + "canonical_slug": "foundry-virtual-tabletop" + }, + { + "slug": "tinymediamanager", + "kind": "duplicate", + "name": "tinyMediaManager", + "source_code_url": "https://github.com/jlesage/docker-tinymediamanager", + "canonical_slug": "tiny-media-manager" + }, + { + "slug": "musicbrainzpicard", + "kind": "duplicate", + "name": "MusicBrainzPicard", + "source_code_url": "https://github.com/jlesage/docker-musicbrainz-picard", + "canonical_slug": "musicbrainz-picard" + }, + { + "slug": "sqlitebrowser", + "kind": "duplicate", + "name": "sqlitebrowser", + "source_code_url": "https://github.com/linuxserver/docker-sqlitebrowser", + "canonical_slug": "sqlite-browser" + }, + { + "slug": "onetimesecret", + "kind": "duplicate", + "name": "OneTimeSecret", + "source_code_url": "https://github.com/siw36/onetimesecret", + "canonical_slug": "one-time-secret" + }, + { + "slug": "jan", + "kind": "duplicate", + "name": "Jan", + "source_code_url": "https://github.com/janhq/jan", + "canonical_slug": "jan-ai" + }, + { + "slug": "nextcloud-aio-mastercontainer", + "kind": "duplicate", + "name": "Nextcloud All-in-One", + "source_code_url": "https://github.com/nextcloud/all-in-one", + "canonical_slug": "nextcloud" + }, + { + "slug": "sentry-self-hosted", + "kind": "duplicate", + "name": "Sentry Self-Hosted", + "source_code_url": "https://github.com/getsentry/self-hosted", + "canonical_slug": "sentry" + }, + { + "slug": "collabora", + "kind": "duplicate", + "name": "Collabora Online", + "source_code_url": "https://gerrit.collaboraoffice.com/core", + "canonical_slug": "collabora-code" + } + ], + "decisions": [ + { + "group": "github.com/seerr-team/seerr", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "seerr", + "retire": [ + "binhex-seerr", + "jellyseerr" + ], + "reason": "Community image repackages the same upstream app. Keep the upstream product record.", + "sources": [ + "https://github.com/seerr-team/seerr", + "https://docs.seerr.dev", + "https://github.com/binhex/documentation" + ], + "facts_to_preserve": [ + { + "slug": "binhex-seerr", + "missing_on_keep": { + "docker_image": "ghcr.io/binhex/arch-seerr" + }, + "retired_ram_estimates": { + "app_id": "972b8afe-7a5c-498f-b64a-fe662f88cdd4", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.758+00:00", + "created_at": "2026-09-02T11:12:41.26667+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/binhex/arch-seerr", + "https://seerr.dev/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "One small media-request service connected to a media server and request manager.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 512, + "storage_source_urls": [ + "https://github.com/binhex/arch-seerr", + "https://seerr.dev/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Application database, settings, and cached metadata.", + "ram_estimate_confidence": "medium", + "storage_estimate_max_gb": 10, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "medium" + }, + "retired_docker_support": "community", + "retired_docker_image": "ghcr.io/binhex/arch-seerr", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + }, + { + "slug": "jellyseerr", + "missing_on_keep": { + "docker_image": "ghcr.io/seerr-team/seerr:v3.4.1@sha256:f4768de5f616248d723e05891f3345a1402123775d03bf0890dbfedc0831bda1", + "ram_default_limit_mb": 4096 + }, + "retired_ram_estimates": { + "app_id": "ae8fe7c8-b229-48a9-99a9-dc29b1653fc7", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.764+00:00", + "created_at": "2026-09-01T07:06:38.767003+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://docs.seerr.dev/getting-started/docker/", + "https://docs.seerr.dev/extending-seerr/database-config/", + "https://docs.seerr.dev/migration-guide/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Legacy Jellyseerr request manager under the unified Seerr codebase, using SQLite for a small household; media files remain in the linked media services.", + "ram_estimate_max_mb": 1024, + "ram_estimate_min_mb": 512, + "storage_source_urls": [ + "https://docs.seerr.dev/getting-started/docker/", + "https://docs.seerr.dev/extending-seerr/database-config/", + "https://docs.seerr.dev/migration-guide/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Seerr configuration, SQLite request history, metadata cache, and logs; linked media libraries are stored elsewhere.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 10, + "storage_estimate_min_gb": 2, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "official", + "retired_docker_image": "ghcr.io/seerr-team/seerr:v3.4.1@sha256:f4768de5f616248d723e05891f3345a1402123775d03bf0890dbfedc0831bda1", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/jellyfin/jellyfin", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "jellyfin", + "retire": [ + "binhex-jellyfin" + ], + "reason": "Community image repackages the same upstream app. Keep the upstream product record.", + "sources": [ + "https://github.com/jellyfin/jellyfin", + "https://jellyfin.org", + "https://github.com/jellyfin/jellyfin#readme", + "https://github.com/binhex/documentation" + ], + "facts_to_preserve": [ + { + "slug": "binhex-jellyfin", + "missing_on_keep": { + "docker_url": "https://github.com/binhex/arch-jellyfin/pkgs/container/arch-jellyfin" + }, + "retired_ram_estimates": { + "app_id": "ffb7f34e-5728-4025-a80d-f8b6d65465ec", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.758+00:00", + "created_at": "2026-09-02T11:12:41.26667+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/binhex/arch-jellyfin", + "https://jellyfin.org/docs/general/installation/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small Jellyfin server for a household library without multiple simultaneous software transcodes.", + "ram_estimate_max_mb": 6144, + "ram_estimate_min_mb": 2048, + "storage_source_urls": [ + "https://github.com/binhex/arch-jellyfin", + "https://jellyfin.org/docs/general/installation/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Jellyfin database, artwork, and metadata cache; the video and music library is mounted separately.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 25, + "storage_estimate_min_gb": 5, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "ghcr.io/binhex/arch-jellyfin", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/catoth/antragsgruen", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "motion-tools-antragsgr-n", + "retire": [ + "motion-tools-antragsgrun" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/CatoTH/antragsgruen", + "https://motion.tools", + "https://github.com/CatoTH/antragsgruen#readme" + ], + "facts_to_preserve": [ + { + "slug": "motion-tools-antragsgrun", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "0123ddc5-5ff4-4b1d-9f11-da0fbd7c539a", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T09:31:18.86036+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/CatoTH/antragsgruen" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small single-node service; data volume and request load vary.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 2048, + "storage_source_urls": [ + "https://github.com/CatoTH/antragsgruen" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Small single-node service; data volume and request load vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 100, + "storage_estimate_min_gb": 5, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "official", + "retired_docker_image": "tobiashoessl/antragsgruen:latest", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/u-wave/core", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "uwave", + "retire": [ + "wave" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/u-wave/core", + "https://u-wave.net", + "https://github.com/u-wave/core#readme", + "https://u-wave.net/" + ], + "facts_to_preserve": [ + { + "slug": "wave", + "missing_on_keep": { + "docker_url": "https://github.com/u-wave/core/blob/HEAD/Dockerfile", + "docker_support": "official" + }, + "retired_ram_estimates": { + "app_id": "b460095e-317e-4ec7-9f5b-67935ea1f298", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-01T07:06:38.767003+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/u-wave/core" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small single-node service; active users and stored content vary.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 1024, + "storage_source_urls": [ + "https://github.com/u-wave/core" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Small single-node service; active users and stored content vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 500, + "storage_estimate_min_gb": 50, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "official", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/backdrop/backdrop", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "backdrop-cms", + "retire": [ + "backdrop" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/backdrop/backdrop", + "https://backdropcms.org/", + "https://backdropcms.org", + "https://github.com/backdrop/backdrop#readme" + ], + "facts_to_preserve": [ + { + "slug": "backdrop", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "fbcd4383-02b4-43b5-854d-039878e24a80", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.758+00:00", + "created_at": "2026-09-02T10:29:40.593779+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/backdrop/backdrop" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small PHP CMS with MySQL; traffic, modules, and image processing raise memory use.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 512, + "storage_source_urls": [ + "https://github.com/backdrop/backdrop" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Small website database, installed files, and a modest media library.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 50, + "storage_estimate_min_gb": 5, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/gladysassistant/gladys", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "gladys-assistant", + "retire": [ + "gladys" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/GladysAssistant/Gladys", + "https://gladysassistant.com", + "https://github.com/GladysAssistant/Gladys#readme", + "https://github.com/gladysassistant/gladys", + "https://gladysassistant.com/", + "https://github.com/gladysassistant/gladys#readme" + ], + "facts_to_preserve": [ + { + "slug": "gladys", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "8c16cdff-834c-48aa-8fb6-50642de20790", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.762+00:00", + "created_at": "2026-09-01T07:06:38.767003+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/GladysAssistant/Gladys", + "https://gladysassistant.com/docs/installation/docker-compose/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small home-automation server with its database, integrations, and a modest number of devices.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 512, + "storage_source_urls": [ + "https://github.com/GladysAssistant/Gladys", + "https://gladysassistant.com/docs/installation/docker-compose/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Configuration, device history, and automation data for one home; retained history changes capacity needs.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 10, + "storage_estimate_min_gb": 2, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "official", + "retired_docker_image": "gladysassistant/gladys:v5", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/capsoftware/cap", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "cap", + "retire": [ + "cap-so" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/CapSoftware/Cap", + "https://cap.so", + "https://cap.so/docs", + "https://cap.so/", + "https://github.com/CapSoftware/Cap#readme" + ], + "facts_to_preserve": [ + { + "slug": "cap-so", + "missing_on_keep": { + "docker_url": "https://github.com/CapSoftware/Cap/blob/main/docker-compose.yml" + }, + "retired_ram_estimates": { + "app_id": "db390e9b-47c9-4553-9d52-ed17351491bf", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.76+00:00", + "created_at": "2026-09-02T09:31:18.86036+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/CapSoftware/Cap", + "https://cap.so/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small Cap recording server handling a few screen and camera recordings.", + "ram_estimate_max_mb": 6144, + "ram_estimate_min_mb": 2048, + "storage_source_urls": [ + "https://github.com/CapSoftware/Cap", + "https://cap.so/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Recorded videos, thumbnails, and app metadata for a small team.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 100, + "storage_estimate_min_gb": 20, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "official", + "retired_docker_image": "ghcr.io/capsoftware/cap-web:latest", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/openg2p/openg2p-spar-self-service", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "openg2p-spar-self-service-api", + "retire": [ + "openg2p-selfservice-api" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/OpenG2P/openg2p-spar-self-service", + "https://openg2p.org" + ], + "facts_to_preserve": [ + { + "slug": "openg2p-selfservice-api", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "673a8712-4b6b-450b-9782-82d5a456a154", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T13:55:32.737363+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://openg2p.org" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 2048, + "storage_source_urls": [ + "https://openg2p.org" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 20, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "openg2p/openg2p-selfservice-api", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/invoiceninja/invoiceninja", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "invoice-ninja", + "retire": [ + "invoiceninja5" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/invoiceninja/invoiceninja", + "https://invoiceninja.org", + "https://invoiceninja.github.io/", + "https://invoiceninja.com", + "https://invoiceninja.github.io/docs/self-host/self-host-installation" + ], + "facts_to_preserve": [ + { + "slug": "invoiceninja5", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "5da13543-d77e-4707-827b-2a90cf64a251", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.764+00:00", + "created_at": "2026-09-02T10:49:43.159579+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/invoiceninja/invoiceninja" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small Laravel invoicing service with a SQL database; the web runtime and database need headroom.", + "ram_estimate_max_mb": 4096, + "ram_estimate_min_mb": 2048, + "storage_source_urls": [ + "https://github.com/invoiceninja/invoiceninja" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Invoice records, generated PDFs, and uploaded logos or attachments drive storage.", + "ram_estimate_confidence": "medium", + "storage_estimate_max_gb": 30, + "storage_estimate_min_gb": 5, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/owntracks/frontend", + "match": "repository", + "classification": "unsure", + "confidence": "low", + "keep": [ + "owntracks", + "owntracks-frontend" + ], + "retire": [], + "reason": "OwnTracks umbrella product may mean recorder plus frontend; shared frontend repository is insufficient to establish same deployment." + }, + { + "group": "github.com/cupcakearmy/cryptgeon", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "cryptgeon", + "retire": [ + "cryptgeon-redis" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/cupcakearmy/cryptgeon", + "https://forums.unraid.net/topic/128263-support-lamp-cryptgeon_redis/", + "https://cryptgeon.org" + ], + "facts_to_preserve": [ + { + "slug": "cryptgeon-redis", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "610bbe3d-7ca5-44d8-95f7-3a9bdd4e4bbf", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T11:21:16.091357+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://forums.unraid.net/topic/128263-support-lamp-cryptgeon_redis/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small single-node service; data volume and request load vary.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 2048, + "storage_source_urls": [ + "https://forums.unraid.net/topic/128263-support-lamp-cryptgeon_redis/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Small single-node service; data volume and request load vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 100, + "storage_estimate_min_gb": 5, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "cupcakearmy/cryptgeon", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/wasi-master/13ft", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "13ft", + "retire": [ + "13ft-ladder" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/wasi-master/13ft", + "https://13ft-ladder.vercel.app", + "https://github.com/wasi-master/13ft#readme" + ], + "facts_to_preserve": [ + { + "slug": "13ft-ladder", + "missing_on_keep": { + "website_url": "https://13ft-ladder.vercel.app", + "categories": [ + "security-privacy" + ] + }, + "retired_ram_estimates": { + "app_id": "9e378207-9c58-4972-bac1-35cc3084c539", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.756+00:00", + "created_at": "2026-09-01T07:06:38.767003+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/wasi-master/13ft#readme", + "https://github.com/wasi-master/13ft/blob/HEAD/docker-compose.yaml" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small proxy interface; concurrent page fetching drives memory use.", + "ram_estimate_max_mb": 1024, + "ram_estimate_min_mb": 256, + "storage_source_urls": [ + "https://github.com/wasi-master/13ft#readme", + "https://github.com/wasi-master/13ft/blob/HEAD/docker-compose.yaml" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "App files, config, and modest logs; no large retained library is described.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 2, + "storage_estimate_min_gb": 0.1, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "official", + "retired_docker_image": "ghcr.io/wasi-master/13ft:latest", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/highlight/highlight", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "highlight", + "retire": [ + "highlight-io" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/highlight/highlight", + "https://highlight.io", + "https://github.com/highlight/highlight#readme", + "https://app.highlight.io", + "https://www.highlight.io/docs/general/company/open-source/hosting/self-host-hobby" + ], + "facts_to_preserve": [ + { + "slug": "highlight-io", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "22115e23-79c9-4f4a-bdfd-54420a56dff1", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T09:31:18.86036+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/highlight/highlight" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 2048, + "storage_source_urls": [ + "https://github.com/highlight/highlight" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 100, + "storage_estimate_min_gb": 5, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/sabre-io/baikal", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "baikal", + "retire": [ + "ba-kal" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/sabre-io/Baikal", + "https://sabre.io/baikal/", + "https://sabre.io/dav/", + "https://sabre.io/baikal", + "https://github.com/sabre-io/Baikal#readme" + ], + "facts_to_preserve": [ + { + "slug": "ba-kal", + "missing_on_keep": { + "docker_image": "ghcr.io/aalmenar/baikal:0.11.1-nginx@sha256:f83c70831ebe2ca9ba84ba7b60927f25941f0fba88bdea6bbebe1d0464bb2aa7", + "compose_url": "https://raw.githubusercontent.com/getumbrel/umbrel-apps/master/baikal/docker-compose.yml", + "docker_support": "community" + }, + "retired_ram_estimates": { + "app_id": "b48b3e8b-9214-46c3-add8-afbfc6bffd6d", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.758+00:00", + "created_at": "2026-09-01T07:06:38.767003+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/sabre-io/Baikal", + "https://sabre.io/baikal/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "A small CalDAV/CardDAV server for a household calendar and contact collection.", + "ram_estimate_max_mb": 256, + "ram_estimate_min_mb": 128, + "storage_source_urls": [ + "https://github.com/sabre-io/Baikal", + "https://sabre.io/baikal/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "A small CalDAV/CardDAV server for a household calendar and contact collection.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 4, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "ghcr.io/aalmenar/baikal:0.11.1-nginx@sha256:f83c70831ebe2ca9ba84ba7b60927f25941f0fba88bdea6bbebe1d0464bb2aa7", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/luanti-org/luanti", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "luanti", + "retire": [ + "minetest" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/luanti-org/luanti", + "https://www.luanti.org/", + "https://www.luanti.org", + "https://github.com/luanti-org/luanti#readme" + ], + "facts_to_preserve": [ + { + "slug": "minetest", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "e7464c85-1e20-4b3b-bba3-081a76642d7f", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T10:49:43.159579+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/luanti-org/luanti" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small single-node service; active users and stored content vary.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 1024, + "storage_source_urls": [ + "https://github.com/luanti-org/luanti" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Small single-node service; active users and stored content vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 100, + "storage_estimate_min_gb": 10, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/bitmagnet-io/bitmagnet", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "bitmagnet", + "retire": [ + "binhex-bitmagnet" + ], + "reason": "Community image repackages the same upstream app. Keep the upstream product record.", + "sources": [ + "https://github.com/bitmagnet-io/bitmagnet", + "https://github.com/binhex/documentation", + "https://bitmagnet.io", + "https://github.com/bitmagnet-io/bitmagnet#readme" + ], + "facts_to_preserve": [ + { + "slug": "binhex-bitmagnet", + "missing_on_keep": { + "categories": [ + "media-servers" + ] + }, + "retired_ram_estimates": { + "app_id": "57ea87b1-30aa-44e9-8849-2da8ec130153", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.758+00:00", + "created_at": "2026-09-02T11:08:48.256264+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/binhex/arch-bitmagnet", + "https://github.com/bitmagnet-io/bitmagnet", + "https://bitmagnet.io/faq.html" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "A small DHT crawl includes Bitmagnet plus PostgreSQL; its FAQ budgets about 300 MB for the app and 1 GB for PostgreSQL.", + "ram_estimate_max_mb": 4096, + "ram_estimate_min_mb": 1536, + "storage_source_urls": [ + "https://github.com/binhex/arch-bitmagnet", + "https://github.com/bitmagnet-io/bitmagnet", + "https://bitmagnet.io/faq.html" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "A few million indexed torrents; the project FAQ estimates about 80 GB per 10 million torrents.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 120, + "storage_estimate_min_gb": 40, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "ghcr.io/binhex/arch-bitmagnet", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/coder/code-server", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "code-server", + "retire": [ + "binhex-code-server" + ], + "reason": "Community image repackages the same upstream app. Keep the upstream product record.", + "sources": [ + "https://github.com/coder/code-server", + "https://github.com/binhex/documentation", + "https://coder.com/", + "https://coder.com/docs/code-server" + ], + "facts_to_preserve": [ + { + "slug": "binhex-code-server", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "c3ef8859-3be7-4e4a-8391-1a47aebd1882", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.758+00:00", + "created_at": "2026-09-02T11:08:48.256264+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/binhex/arch-code-server", + "https://coder.com/docs/code-server" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "One browser-based VS Code session with a few extensions and light builds; the upstream guide lists 1 GB and 2 vCPUs as its starting point.", + "ram_estimate_max_mb": 6144, + "ram_estimate_min_mb": 2048, + "storage_source_urls": [ + "https://github.com/binhex/arch-code-server", + "https://coder.com/docs/code-server" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "One workspace, installed extensions, and language-server/build caches.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 50, + "storage_estimate_min_gb": 20, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "ghcr.io/binhex/arch-code-server", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/filebrowser/filebrowser", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "filebrowser", + "retire": [ + "binhex-filebrowser" + ], + "reason": "Community image repackages the same upstream app. Keep the upstream product record.", + "sources": [ + "https://github.com/filebrowser/filebrowser", + "https://github.com/filebrowser/filebrowser/tree/master/docs", + "https://filebrowser.org", + "https://github.com/filebrowser/filebrowser#readme" + ], + "facts_to_preserve": [ + { + "slug": "binhex-filebrowser", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "fcdfbe55-da6a-4ad0-a83f-66b273ea3676", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.758+00:00", + "created_at": "2026-09-02T11:12:41.26667+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/binhex/arch-filebrowser", + "https://github.com/filebrowser/filebrowser" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "One File Browser instance serving a small number of users and a mounted directory tree.", + "ram_estimate_max_mb": 512, + "ram_estimate_min_mb": 128, + "storage_source_urls": [ + "https://github.com/binhex/arch-filebrowser", + "https://github.com/filebrowser/filebrowser" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "File Browser configuration and database; user files stay in the mounted host directory.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 10, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "ghcr.io/binhex/arch-filebrowser", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/flaresolverr/flaresolverr", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "flaresolverr", + "retire": [ + "binhex-flaresolverr" + ], + "reason": "Community image repackages the same upstream app. Keep the upstream product record.", + "sources": [ + "https://github.com/FlareSolverr/FlareSolverr", + "https://github.com/binhex/documentation", + "https://github.com/FlareSolverr/FlareSolverr#readme" + ], + "facts_to_preserve": [ + { + "slug": "binhex-flaresolverr", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "b95c6793-74a7-49b4-9c55-0eb37ce84905", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.758+00:00", + "created_at": "2026-09-02T11:12:41.26667+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/binhex/arch-flaresolverr", + "https://github.com/FlareSolverr/FlareSolverr" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "A small FlareSolverr service handles a few concurrent Chromium-based browser challenges.", + "ram_estimate_max_mb": 4096, + "ram_estimate_min_mb": 1024, + "storage_source_urls": [ + "https://github.com/binhex/arch-flaresolverr", + "https://github.com/FlareSolverr/FlareSolverr" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Browser profile, temporary challenge data, and logs; fetched pages are not retained as an archive.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 10, + "storage_estimate_min_gb": 2, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "ghcr.io/binhex/arch-flaresolverr", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/maybe-finance/maybe", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "maybe", + "retire": [ + "maybe-web" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/maybe-finance/maybe", + "https://maybefinance.com/", + "https://github.com/dkeners/unraid-templates/blob/main/README.md" + ], + "facts_to_preserve": [ + { + "slug": "maybe-web", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "3bbb0cb3-ca14-4ffe-82df-c478eb8ec125", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T12:06:19.442107+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://maybefinance.com/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 256, + "storage_source_urls": [ + "https://maybefinance.com/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 500, + "storage_estimate_min_gb": 50, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "ghcr.io/maybe-finance/maybe:latest", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/henrygd/beszel", + "match": "repository", + "classification": "distinct", + "confidence": "high", + "keep": [ + "beszel-agent", + "beszel" + ], + "retire": [], + "reason": "Hub and metrics agent are separately deployed components (https://beszel.dev/guide/compiling)." + }, + { + "group": "github.com/conversejs/converse.js", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "converse-js", + "retire": [ + "converse" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/conversejs/converse.js", + "http://conversejs.org", + "https://github.com/conversejs/converse.js#readme", + "https://conversejs.org/docs/html/index.html" + ], + "facts_to_preserve": [ + { + "slug": "converse", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "c2fa90c4-6817-459f-8a21-caa5104bf2e2", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.761+00:00", + "created_at": "2026-09-02T10:29:40.593779+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/conversejs/converse.js", + "https://conversejs.org/docs/html/index.html" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Converse is a client-side web app; a small static web host serves the client bundle.", + "ram_estimate_max_mb": 512, + "ram_estimate_min_mb": 128, + "storage_source_urls": [ + "https://github.com/conversejs/converse.js", + "https://conversejs.org/docs/html/index.html" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Client files and web configuration; chat history and uploads are handled by the XMPP service.", + "ram_estimate_confidence": "medium", + "storage_estimate_max_gb": 5, + "storage_estimate_min_gb": 0.5, + "storage_estimate_confidence": "medium" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/navidrome/navidrome", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "navidrome", + "retire": [ + "navidrome-music-server" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/navidrome/navidrome", + "https://www.navidrome.org", + "https://www.navidrome.org/docs", + "https://github.com/navidrome/navidrome#readme" + ], + "facts_to_preserve": [ + { + "slug": "navidrome-music-server", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "5463eb94-de6a-4e2c-aaa7-9976c4d14227", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.767+00:00", + "created_at": "2026-09-01T07:06:38.767003+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://www.navidrome.org/docs/installation/docker/", + "https://www.navidrome.org/docs/usage/configuration/options/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Lightweight music server for a few listeners; library scan size and concurrent streams drive memory.", + "ram_estimate_max_mb": 1024, + "ram_estimate_min_mb": 256, + "storage_source_urls": [ + "https://www.navidrome.org/docs/installation/docker/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Includes app database/cache plus a small-to-large personal music library mounted at /music.", + "ram_estimate_confidence": "medium", + "storage_estimate_max_gb": 500, + "storage_estimate_min_gb": 20, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "official", + "retired_docker_image": "deluan/navidrome:latest", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/lidarr/lidarr", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "lidarr", + "retire": [ + "binhex-lidarr" + ], + "reason": "Community image repackages the same upstream app. Keep the upstream product record.", + "sources": [ + "https://github.com/Lidarr/Lidarr", + "https://github.com/binhex/documentation", + "http://lidarr.audio", + "https://github.com/Lidarr/Lidarr#readme" + ], + "facts_to_preserve": [ + { + "slug": "binhex-lidarr", + "missing_on_keep": { + "docker_url": "https://github.com/binhex/arch-lidarr/pkgs/container/arch-lidarr" + }, + "retired_ram_estimates": { + "app_id": "bb8ddb63-ec4f-4381-ae04-e5e763a156c3", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.758+00:00", + "created_at": "2026-09-02T11:12:41.26667+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://wiki.servarr.com/lidarr/quick-start-guide", + "https://github.com/binhex/arch-lidarr" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "One small music library manager tracking a few thousand releases, with downloads handled by a separate client.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 512, + "storage_source_urls": [ + "https://wiki.servarr.com/lidarr/quick-start-guide", + "https://github.com/binhex/arch-lidarr" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Lidarr configuration, SQLite database, logs, and metadata; the managed music files are separate.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 10, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "ghcr.io/binhex/arch-lidarr", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/nginx/nginx", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "nginx", + "retire": [ + "binhex-nginx" + ], + "reason": "Community image repackages the same upstream app. Keep the upstream product record.", + "sources": [ + "https://github.com/nginx/nginx", + "https://github.com/binhex/documentation", + "https://nginx.org", + "https://github.com/nginx/nginx#readme" + ], + "facts_to_preserve": [ + { + "slug": "binhex-nginx", + "missing_on_keep": { + "docker_url": "https://github.com/binhex/arch-nginx/pkgs/container/arch-nginx", + "docker_image": "ghcr.io/binhex/arch-nginx", + "docker_support": "community" + }, + "retired_ram_estimates": { + "app_id": "a4c1d784-d919-4c90-8bd3-967755471695", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.758+00:00", + "created_at": "2026-09-02T11:12:41.26667+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/binhex/arch-nginx", + "https://nginx.org/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small web server or reverse proxy with modest traffic and a few routes.", + "ram_estimate_max_mb": 512, + "ram_estimate_min_mb": 128, + "storage_source_urls": [ + "https://github.com/binhex/arch-nginx", + "https://nginx.org/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Configuration and logs; cached or hosted files can grow with use.", + "ram_estimate_confidence": "medium", + "storage_estimate_max_gb": 10, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "medium" + }, + "retired_docker_support": "community", + "retired_docker_image": "ghcr.io/binhex/arch-nginx", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/rspamd/rspamd", + "match": "repository", + "classification": "distinct", + "confidence": "high", + "keep": [ + "rspamd", + "rspamdui" + ], + "retire": [], + "reason": "Spam engine and its web administration interface have different purposes." + }, + { + "group": "github.com/nzbgetcom/nzbget", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "nzbget", + "retire": [ + "binhex-nzbget" + ], + "reason": "Community image repackages the same upstream app. Keep the upstream product record.", + "sources": [ + "https://github.com/nzbgetcom/nzbget", + "https://github.com/binhex/documentation", + "https://nzbget.com", + "https://github.com/nzbgetcom/nzbget#readme" + ], + "facts_to_preserve": [ + { + "slug": "binhex-nzbget", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "e6696f79-3a7a-44d3-a2e0-0a2d7863886c", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.758+00:00", + "created_at": "2026-09-02T11:12:41.26667+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/binhex/arch-nzbget", + "https://nzbget.com/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "One home Usenet download queue; concurrent jobs and repair/unpack work affect memory.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 256, + "storage_source_urls": [ + "https://github.com/binhex/arch-nzbget", + "https://nzbget.com/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Application state plus a modest download and temporary-work area; scale with queue size.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 200, + "storage_estimate_min_gb": 10, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "ghcr.io/binhex/arch-nzbget", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/miroslavpejic85/mirotalk", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "mirotalk-p2p", + "retire": [ + "mirotalk" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/miroslavpejic85/mirotalk", + "https://meet.no42.org/", + "https://docs.mirotalk.com/", + "https://p2p.mirotalk.com", + "https://github.com/miroslavpejic85/mirotalk#readme" + ], + "facts_to_preserve": [ + { + "slug": "mirotalk", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "0998b86c-f6ff-4ea6-a205-d69cece5d2d7", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T10:49:43.159579+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/miroslavpejic85/mirotalk" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small single-node service; active users and stored content vary.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 1024, + "storage_source_urls": [ + "https://github.com/miroslavpejic85/mirotalk" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Small single-node service; active users and stored content vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 20, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/ddnet/ddnet", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "ddracenetwork", + "retire": [ + "ddnet" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/ddnet/ddnet", + "https://ddnet.tw/", + "https://forums.unraid.net/topic/79530-support-ich777-gameserver-dockers/", + "https://ddnet.org", + "https://github.com/ddnet/ddnet#readme" + ], + "facts_to_preserve": [ + { + "slug": "ddnet", + "missing_on_keep": { + "docker_url": "https://hub.docker.com/r/ich777/ddnetserver/", + "docker_image": "ghcr.io/ich777/ddnetserver", + "docker_support": "community" + }, + "retired_ram_estimates": { + "app_id": "d6c3c8c6-f830-4a70-bd59-3d86654014de", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.761+00:00", + "created_at": "2026-09-02T11:21:16.091357+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/ddnet/ddnet", + "https://ddnet.org/server/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small DDNet dedicated server hosts a modest player count and game state.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 512, + "storage_source_urls": [ + "https://github.com/ddnet/ddnet", + "https://ddnet.org/server/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Server maps, configuration, logs, and optional demos.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 20, + "storage_estimate_min_gb": 2, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "ghcr.io/ich777/ddnetserver", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/rclone/rclone", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "rclone", + "retire": [ + "binhex-rclone" + ], + "reason": "Community image repackages the same upstream app. Keep the upstream product record.", + "sources": [ + "https://github.com/rclone/rclone", + "https://github.com/binhex/documentation", + "https://rclone.org/", + "https://rclone.org/docs/" + ], + "facts_to_preserve": [ + { + "slug": "binhex-rclone", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "1d1e9982-37de-43d7-a3d3-401b22eab26e", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.758+00:00", + "created_at": "2026-09-02T11:12:41.26667+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/binhex/arch-rclone", + "https://rclone.org/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Single-user file-transfer or mount workload; parallel transfers and caches affect memory.", + "ram_estimate_max_mb": 1024, + "ram_estimate_min_mb": 256, + "storage_source_urls": [ + "https://github.com/binhex/arch-rclone", + "https://rclone.org/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Configuration, logs, and optional local cache; remote files are stored at their destination.", + "ram_estimate_confidence": "medium", + "storage_estimate_max_gb": 10, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "medium" + }, + "retired_docker_support": "community", + "retired_docker_image": "ghcr.io/binhex/arch-rclone", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/slskd/slskd", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "slskd", + "retire": [ + "binhex-slskd" + ], + "reason": "Community image repackages the same upstream app. Keep the upstream product record.", + "sources": [ + "https://github.com/slskd/slskd", + "https://github.com/binhex/documentation", + "https://slskd.org", + "https://github.com/slskd/slskd#readme" + ], + "facts_to_preserve": [ + { + "slug": "binhex-slskd", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "fe915cab-0169-4460-b252-06da9fd63fe0", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.758+00:00", + "created_at": "2026-09-02T11:12:41.26667+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/binhex/arch-slskd", + "https://github.com/slskd/slskd" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "One small Soulseek node; peer count, searches, and queued transfers affect memory.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 512, + "storage_source_urls": [ + "https://github.com/binhex/arch-slskd", + "https://github.com/slskd/slskd" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Allow for shared and downloaded files plus application state; file volume grows with use.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 500, + "storage_estimate_min_gb": 10, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "ghcr.io/binhex/arch-slskd", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/jgraph/drawio", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "draw-io", + "retire": [ + "diagramsnet" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/jgraph/drawio", + "https://www.diagrams.net/", + "https://www.drawio.com/docs/", + "https://www.drawio.com", + "https://github.com/jgraph/drawio#readme" + ], + "facts_to_preserve": [ + { + "slug": "diagramsnet", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "7c4a8ab4-c409-402e-85ab-1d42cf9b699c", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.761+00:00", + "created_at": "2026-09-02T10:34:27.159229+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/jgraph/drawio" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "One self-hosted diagrams.net editor instance with ordinary static assets and a modest saved diagram set.", + "ram_estimate_max_mb": 512, + "ram_estimate_min_mb": 256, + "storage_source_urls": [ + "https://github.com/jgraph/drawio" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "One self-hosted diagrams.net editor instance with ordinary static assets and a modest saved diagram set.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 5, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/syncthing/syncthing", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "syncthing", + "retire": [ + "binhex-syncthing" + ], + "reason": "Community image repackages the same upstream app. Keep the upstream product record.", + "sources": [ + "https://github.com/syncthing/syncthing", + "https://github.com/binhex/documentation", + "https://syncthing.net/", + "https://docs.syncthing.net/" + ], + "facts_to_preserve": [ + { + "slug": "binhex-syncthing", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "e7871675-0f19-4536-9c78-ee3a0442a114", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.758+00:00", + "created_at": "2026-09-02T11:12:41.26667+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/binhex/arch-syncthing", + "https://syncthing.net/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "One small synchronization node; file count, indexing, and active connections affect memory.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 512, + "storage_source_urls": [ + "https://github.com/binhex/arch-syncthing", + "https://syncthing.net/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Allow for synchronized folders and file versions; capacity follows the chosen folders.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 500, + "storage_estimate_min_gb": 10, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "ghcr.io/binhex/arch-syncthing", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/bitwarden/server", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "bitwarden", + "retire": [ + "bitwarden-lite" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/bitwarden/server", + "https://bitwarden.com", + "https://bitwarden.com/help/install-on-premise-linux/", + "https://bitwarden.com/help/install-and-deploy-lite/" + ], + "facts_to_preserve": [ + { + "slug": "bitwarden-lite", + "missing_on_keep": { + "ram_min_mb": 200 + }, + "retired_ram_estimates": { + "app_id": "8d3ecf4a-3afa-4588-8650-65b116097513", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.759+00:00", + "created_at": "2026-09-02T11:12:41.26667+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/mimartin12/unraid-bw-lite-template", + "https://bitwarden.com/help/install-and-deploy-lite/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small Bitwarden Lite home deployment; official minimum is 200 MB and database services add memory.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 512, + "storage_source_urls": [ + "https://github.com/mimartin12/unraid-bw-lite-template", + "https://bitwarden.com/help/install-and-deploy-lite/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Official minimum is 1 GB; allow for the vault database, attachments, and backups.", + "ram_estimate_confidence": "medium", + "storage_estimate_max_gb": 20, + "storage_estimate_min_gb": 2, + "storage_estimate_confidence": "medium" + }, + "retired_docker_support": "official", + "retired_docker_image": "ghcr.io/bitwarden/lite:latest", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/sentriz/gonic", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "gonic", + "retire": [ + "binhex-gonic" + ], + "reason": "Community image repackages the same upstream app. Keep the upstream product record.", + "sources": [ + "https://github.com/sentriz/gonic", + "https://github.com/binhex/documentation", + "https://web.libera.chat/?channels=#gonic", + "https://github.com/sentriz/gonic#readme" + ], + "facts_to_preserve": [ + { + "slug": "binhex-gonic", + "missing_on_keep": { + "docker_image": "ghcr.io/binhex/arch-gonic" + }, + "retired_ram_estimates": { + "app_id": "d3556703-25f3-41d1-bb25-96b180eba4db", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.758+00:00", + "created_at": "2026-09-02T11:12:41.26667+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/binhex/arch-gonic", + "https://github.com/sentriz/gonic" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "A small gonic music server indexes one household music library and serves a few clients.", + "ram_estimate_max_mb": 1024, + "ram_estimate_min_mb": 256, + "storage_source_urls": [ + "https://github.com/binhex/arch-gonic", + "https://github.com/sentriz/gonic" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Music index, artwork, and transcode cache; original music files are mounted separately.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 20, + "storage_estimate_min_gb": 5, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "ghcr.io/binhex/arch-gonic", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/borgbackup/borg", + "match": "repository", + "classification": "unsure", + "confidence": "low", + "keep": [ + "borgserver", + "borg" + ], + "retire": [], + "reason": "Borg server container may add a distinct managed backup storage service; review its packaging before merging with Borg CLI." + }, + { + "group": "github.com/jitsi/jitsi-meet", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "jitsi-meet", + "retire": [ + "jitsi" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/jitsi/jitsi-meet", + "https://jitsi.org/", + "https://jitsi.org/meet", + "https://github.com/jitsi/jitsi-meet#readme" + ], + "facts_to_preserve": [ + { + "slug": "jitsi", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "76e6928e-73f9-4ad5-aa8a-565101138e76", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T10:49:43.159579+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/jitsi/jitsi-meet" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small single-node service; active users and stored content vary.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 1024, + "storage_source_urls": [ + "https://github.com/jitsi/jitsi-meet" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Small single-node service; active users and stored content vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 20, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/rustdesk/rustdesk-server", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "rustdesk", + "retire": [ + "rustdesk-server" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/rustdesk/rustdesk-server", + "https://rustdesk.com/", + "https://rustdesk.com/docs/en/" + ], + "facts_to_preserve": [ + { + "slug": "rustdesk-server", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "e184541e-56be-4eff-bd91-c2abf262ae3a", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T10:49:43.159579+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/rustdesk/rustdesk-server" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 256, + "storage_source_urls": [ + "https://github.com/rustdesk/rustdesk-server" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 20, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/laurent22/joplin", + "match": "repository", + "classification": "distinct", + "confidence": "high", + "keep": [ + "joplin-server", + "joplin" + ], + "retire": [], + "reason": "Joplin synchronization server and browser-packaged note client are separate components." + }, + { + "group": "github.com/dgtlmoon/changedetection.io", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "changedetection-io", + "retire": [ + "changedetection" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/dgtlmoon/changedetection.io", + "https://changedetection.io", + "https://github.com/dgtlmoon/changedetection.io#readme" + ], + "facts_to_preserve": [ + { + "slug": "changedetection", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "be81ca1f-65fb-44c5-989f-0a5d2dddc6af", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.76+00:00", + "created_at": "2026-09-02T10:29:40.593779+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/dgtlmoon/changedetection.io" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "A small changedetection.io instance watching a few hundred pages on a moderate schedule without browser-heavy checks.", + "ram_estimate_max_mb": 1024, + "ram_estimate_min_mb": 512, + "storage_source_urls": [ + "https://github.com/dgtlmoon/changedetection.io" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "A small changedetection.io instance watching a few hundred pages on a moderate schedule without browser-heavy checks.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 20, + "storage_estimate_min_gb": 2, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/lovasoa/whitebophir", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "wbo", + "retire": [ + "whitebophir" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/lovasoa/whitebophir", + "https://wbo.ophir.dev", + "https://github.com/lovasoa/whitebophir#readme", + "https://wbo.ophir.dev/" + ], + "facts_to_preserve": [ + { + "slug": "whitebophir", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "d18a483d-8b1b-4be4-9bae-2ca6824ace93", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T10:49:43.159579+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/lovasoa/whitebophir" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 256, + "storage_source_urls": [ + "https://github.com/lovasoa/whitebophir" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 500, + "storage_estimate_min_gb": 50, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/readarr/readarr", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "readarr", + "retire": [ + "binhex-readarr" + ], + "reason": "Community image repackages the same upstream app. Keep the upstream product record.", + "sources": [ + "https://github.com/Readarr/Readarr", + "https://github.com/Readarr/Readarr#readme", + "http://readarr.com" + ], + "facts_to_preserve": [ + { + "slug": "binhex-readarr", + "missing_on_keep": { + "docker_url": "https://github.com/binhex/arch-readarr/pkgs/container/arch-readarr" + }, + "retired_ram_estimates": { + "app_id": "9da21cab-c9c2-4e0d-af8a-dda982071404", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.758+00:00", + "created_at": "2026-09-02T11:12:41.26667+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/binhex/arch-readarr", + "https://github.com/Readarr/Readarr" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "One small book-library manager; library size and refresh tasks affect memory.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 512, + "storage_source_urls": [ + "https://github.com/binhex/arch-readarr", + "https://github.com/Readarr/Readarr" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Application database and metadata; ebook and audiobook files use the mapped library volume.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 10, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "medium" + }, + "retired_docker_support": "community", + "retired_docker_image": "ghcr.io/binhex/arch-readarr", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/givanz/vvveb", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "vvveb-cms", + "retire": [ + "vvveb" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/givanz/Vvveb", + "https://www.vvveb.com/", + "https://docs.vvveb.com/installation", + "https://www.vvveb.com", + "https://github.com/givanz/Vvveb#readme" + ], + "facts_to_preserve": [ + { + "slug": "vvveb", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "b75e1e7e-1cc7-4f52-9447-ea8f960b8799", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T10:49:43.159579+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/givanz/Vvveb" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 256, + "storage_source_urls": [ + "https://github.com/givanz/Vvveb" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 20, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/wallabag/wallabag", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "wallabag", + "retire": [ + "wallabag2" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/wallabag/wallabag", + "https://www.wallabag.org", + "https://doc.wallabag.org/en/", + "https://wallabag.org", + "https://github.com/wallabag/wallabag#readme" + ], + "facts_to_preserve": [ + { + "slug": "wallabag2", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "a706be5f-d4a2-42c7-939d-e12cfb012b64", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.769+00:00", + "created_at": "2026-09-02T10:49:43.159579+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/wallabag/wallabag", + "https://github.com/wallabag/docker/blob/master/README.md", + "https://doc.wallabag.org/en/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "One read-it-later service with its web app, database, and a small reader group.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 1024, + "storage_source_urls": [ + "https://github.com/wallabag/wallabag", + "https://github.com/wallabag/docker/blob/master/README.md", + "https://doc.wallabag.org/en/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Saved article text, database records, and article images for a modest reading archive.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 50, + "storage_estimate_min_gb": 5, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/tt-rss/tt-rss", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "tiny-tiny-rss", + "retire": [ + "ttrss" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/tt-rss/tt-rss", + "https://tt-rss.org", + "https://github.com/tt-rss/tt-rss#readme", + "https://github.com/tt-rss/tt-rss/wiki" + ], + "facts_to_preserve": [ + { + "slug": "ttrss", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "6bc5ff3a-fb62-487c-9ba9-dd6aa0f1a002", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T10:49:43.159579+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/tt-rss/tt-rss" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 256, + "storage_source_urls": [ + "https://github.com/tt-rss/tt-rss" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 500, + "storage_estimate_min_gb": 50, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/mantisbt/mantisbt", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "mantisbt", + "retire": [ + "mantis" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/mantisbt/mantisbt", + "https://mantisbt.org/", + "https://mantisbt.org/documentation.php", + "https://mantisbt.org", + "https://github.com/mantisbt/mantisbt#readme" + ], + "facts_to_preserve": [ + { + "slug": "mantis", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "8f010a9e-91fe-4ef8-80b6-c3a12a2b8698", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T10:49:43.159579+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/mantisbt/mantisbt" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 256, + "storage_source_urls": [ + "https://github.com/mantisbt/mantisbt" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 20, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/drakkan/sftpgo", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "sftpgo", + "retire": [ + "sftpgo-community-edition" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/drakkan/sftpgo", + "https://sftpgo.com", + "https://github.com/drakkan/sftpgo#readme", + "https://docs.sftpgo.com/latest/" + ], + "facts_to_preserve": [ + { + "slug": "sftpgo-community-edition", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "6508cf24-7759-4513-bcea-ca888ca643af", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.769+00:00", + "created_at": "2026-09-02T09:31:18.86036+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/drakkan/sftpgo", + "https://docs.sftpgo.com/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "One small SFTPGo service with a few users and low concurrent transfer volume.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 512, + "storage_source_urls": [ + "https://github.com/drakkan/sftpgo", + "https://docs.sftpgo.com/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Files in the configured local storage backend for a small household; user data determines actual use.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 500, + "storage_estimate_min_gb": 50, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "official", + "retired_docker_image": "drakkan/sftpgo:latest", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/suitenumerique/docs", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "docs", + "retire": [ + "lasuite-docs" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/suitenumerique/docs", + "https://docs.numerique.gouv.fr" + ], + "facts_to_preserve": [ + { + "slug": "lasuite-docs", + "missing_on_keep": { + "categories": [ + "documents-pdfs" + ] + }, + "retired_ram_estimates": { + "app_id": "e3f47bbb-73a2-4d73-985a-66d3d28a1e70", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T10:49:43.159579+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/suitenumerique/docs" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 2048, + "storage_source_urls": [ + "https://github.com/suitenumerique/docs" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 500, + "storage_estimate_min_gb": 50, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/kuvasz-uptime/kuvasz", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "kuvasz", + "retire": [ + "kuvasz-uptime" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/kuvasz-uptime/kuvasz", + "https://kuvasz-uptime.dev", + "https://github.com/kuvasz-uptime/kuvasz#readme", + "https://kuvasz-uptime.dev/" + ], + "facts_to_preserve": [ + { + "slug": "kuvasz-uptime", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "a0cf3119-e123-4885-830e-07d86332c844", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T09:31:18.86036+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/kuvasz-uptime/kuvasz" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 2048, + "storage_source_urls": [ + "https://github.com/kuvasz-uptime/kuvasz" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 100, + "storage_estimate_min_gb": 5, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/getumbrel/umbrel", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "umbrelos", + "retire": [ + "umbrel" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/getumbrel/umbrel", + "https://umbrel.com", + "https://umbrel.com/umbrelos", + "https://github.com/getumbrel/umbrel#readme" + ], + "facts_to_preserve": [ + { + "slug": "umbrel", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "9b8a30b5-7205-41ab-a468-7377e0415332", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T09:31:18.86036+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/getumbrel/umbrel" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 2048, + "storage_source_urls": [ + "https://github.com/getumbrel/umbrel" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 500, + "storage_estimate_min_gb": 50, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/docmost/docmost", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "docmost", + "retire": [ + "docmost-community-edition" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/docmost/docmost", + "https://docmost.com", + "https://github.com/docmost/docmost#readme" + ], + "facts_to_preserve": [ + { + "slug": "docmost-community-edition", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "45b7fca6-2f1d-40f9-b239-b21c403f28fe", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T09:31:18.86036+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/docmost/docmost" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 256, + "storage_source_urls": [ + "https://github.com/docmost/docmost" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 500, + "storage_estimate_min_gb": 50, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "official", + "retired_docker_image": "docmost/docmost:latest", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/getwud/wud", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "wud", + "retire": [ + "whats-up-docker" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/getwud/wud", + "https://getwud.github.io/wud", + "https://github.com/getwud/wud#readme", + "https://getwud.app/", + "https://getwud.app/docs/" + ], + "facts_to_preserve": [ + { + "slug": "whats-up-docker", + "missing_on_keep": { + "docker_image": "getwud/wud", + "compose_url": "https://getwud.app/docs/quickstart/" + }, + "retired_ram_estimates": { + "app_id": "0caa0d87-09b9-4677-92b5-0159d6bdab7d", + "ram_note": null, + "checked_at": "2026-09-28T18:52:57.315454+00:00", + "created_at": "2026-09-02T13:04:30.40536+00:00", + "updated_at": "2026-09-28T18:52:57.315454+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://getwud.app/docs/quickstart/", + "https://getwud.app/docs/configuration/storage/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small Docker host monitoring a modest container list and update history.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 512, + "storage_source_urls": [ + "https://getwud.app/docs/configuration/storage/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "SQLite state and image history under /store; watched workload and retained history drive growth.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 20, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "official", + "retired_docker_image": "getwud/wud", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/onlyoffice/documentserver", + "match": "repository", + "classification": "unsure", + "confidence": "low", + "keep": [ + "onlyoffice", + "onlyoffice-docs" + ], + "retire": [], + "reason": "ONLYOFFICE summary describes the broader suite, but repository points to DocumentServer; resolve product identity first." + }, + { + "group": "github.com/comfy-org/comfyui", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "comfyui", + "retire": [ + "comfyui-intel-xpu" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/comfy-org/comfyui", + "https://comfy.org/", + "https://docs.comfy.org/", + "https://github.com/Comfy-Org/ComfyUI", + "https://github.com/heroeswearkapes/comfyui-intel-xpu/issues" + ], + "facts_to_preserve": [ + { + "slug": "comfyui-intel-xpu", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "122ef730-621c-4e50-a94c-727d52bb8dd4", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T11:17:17.463979+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/heroeswearkapes/comfyui-intel-xpu" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small local model workload; model size and outputs vary.", + "ram_estimate_max_mb": 32768, + "ram_estimate_min_mb": 8192, + "storage_source_urls": [ + "https://github.com/heroeswearkapes/comfyui-intel-xpu" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Small local model workload; model size and outputs vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 100, + "storage_estimate_min_gb": 20, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "heroeswearkapes/comfyui-intel-xpu:latest", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/langgenius/dify", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "dify", + "retire": [ + "dify-ai" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/langgenius/dify", + "https://dify.ai", + "https://github.com/langgenius/dify#readme", + "https://dify.ai/", + "https://docs.dify.ai/en/home" + ], + "facts_to_preserve": [ + { + "slug": "dify-ai", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "875c051d-942e-4642-ba6e-096eee1358d0", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.761+00:00", + "created_at": "2026-09-02T09:31:18.86036+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/langgenius/dify", + "https://github.com/langgenius/dify/blob/main/docker/docker-compose.yaml" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "The official Dify Compose stack serving a small team through external model APIs with a modest knowledge base.", + "ram_estimate_max_mb": 16384, + "ram_estimate_min_mb": 8192, + "storage_source_urls": [ + "https://github.com/langgenius/dify", + "https://github.com/langgenius/dify/blob/main/docker/docker-compose.yaml" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "The official Dify Compose stack serving a small team through external model APIs with a modest knowledge base.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 100, + "storage_estimate_min_gb": 10, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "official", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/technitiumsoftware/dnsserver", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "technitium-dns-server", + "retire": [ + "technitium-dns" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/TechnitiumSoftware/DnsServer", + "https://technitium.com/dns/", + "https://technitium.com/dns/help.html", + "https://technitium.com/dns", + "https://github.com/TechnitiumSoftware/DnsServer#readme" + ], + "facts_to_preserve": [ + { + "slug": "technitium-dns", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "9ec73b56-6f18-4743-81c4-80741779968f", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.769+00:00", + "created_at": "2026-09-02T10:49:43.159579+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://technitium.com/dns/", + "https://hub.docker.com/r/technitium/dns-server" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Home or small-office DNS server with ordinary cache, zones, filter lists, and modest query logs.", + "ram_estimate_max_mb": 1024, + "ram_estimate_min_mb": 512, + "storage_source_urls": [ + "https://technitium.com/dns/", + "https://hub.docker.com/r/technitium/dns-server" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Home or small-office DNS server with ordinary cache, zones, filter lists, and modest query logs.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 8, + "storage_estimate_min_gb": 2, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/harness/harness", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "harness", + "retire": [ + "gitness" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/harness/harness", + "https://www.harness.io/open-source", + "https://github.com/harness/harness#readme" + ], + "facts_to_preserve": [ + { + "slug": "gitness", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "83b8a3d3-c29c-44e6-825f-56fb997c0230", + "ram_note": null, + "checked_at": "2026-09-29T08:14:28.648103+00:00", + "created_at": "2026-09-02T11:46:23.046796+00:00", + "updated_at": "2026-09-29T08:14:28.648103+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/harness/harness#readme", + "https://www.harness.io/open-source" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "A small Git and CI service; repository size, pipeline runs, and Docker build containers drive memory use.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 2048, + "storage_source_urls": [ + "https://github.com/harness/harness#readme" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "The /data volume stores the database and repositories; repository history and pipeline artifacts drive growth.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 100, + "storage_estimate_min_gb": 10, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "official", + "retired_docker_image": "harness/harness", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/distribution/distribution", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "distribution", + "retire": [ + "dockerregistry" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/distribution/distribution", + "https://distribution.github.io/distribution/", + "https://distribution.github.io/distribution/about/deploying/" + ], + "facts_to_preserve": [ + { + "slug": "dockerregistry", + "missing_on_keep": { + "compose_url": "https://distribution.github.io/distribution/about/deploying/#deploy-your-registry-using-a-compose-file" + }, + "retired_ram_estimates": { + "app_id": "cad7937d-3765-4266-8bf5-3743b154661a", + "ram_note": null, + "checked_at": "2026-09-28T18:38:50.290423+00:00", + "created_at": "2026-09-02T11:36:12.184365+00:00", + "updated_at": "2026-09-28T18:38:50.290423+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://distribution.github.io/distribution/about/deploying/", + "https://github.com/distribution/distribution" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small private registry with low request volume; concurrent uploads and downloads raise use.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 256, + "storage_source_urls": [ + "https://distribution.github.io/distribution/about/deploying/", + "https://distribution.github.io/distribution/storage-drivers/filesystem/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Small collection of container images; retained tags and layers drive growth.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 500, + "storage_estimate_min_gb": 10, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "official", + "retired_docker_image": "registry:3", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/caronc/apprise", + "match": "repository", + "classification": "unsure", + "confidence": "low", + "keep": [ + "apprise", + "ssh-apprise" + ], + "retire": [], + "reason": "SSH notification integration may be a separate wrapper app with a misassigned upstream repository." + }, + { + "group": "github.com/cisco-talos/clamav", + "match": "repository", + "classification": "distinct", + "confidence": "high", + "keep": [ + "clamav", + "clamdscan" + ], + "retire": [], + "reason": "Antivirus engine and clamdscan one-shot scanning client are different executables." + }, + { + "group": "github.com/moghtech/komodo", + "match": "repository", + "classification": "distinct", + "confidence": "high", + "keep": [ + "komodo", + "komodo-client" + ], + "retire": [], + "reason": "Komodo dashboard and host client are separately deployed components." + }, + { + "group": "git.deuxfleurs.fr/Deuxfleurs/garage", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "garage-s3-simple", + "retire": [ + "garage-s3-tailscale" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://git.deuxfleurs.fr/Deuxfleurs/garage", + "https://discord.gg/p9xkjEw8ts" + ], + "facts_to_preserve": [ + { + "slug": "garage-s3-tailscale", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "d45e07e1-9de4-40e7-b89e-9a930916d1be", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.762+00:00", + "created_at": "2026-09-02T11:43:52.951316+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://garagehq.deuxfleurs.fr/documentation/", + "https://hub.docker.com/r/waazaafr/garage-s3-tailscale" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Single small Garage object-store node with Tailscale access; active requests and replication affect memory.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 1024, + "storage_source_urls": [ + "https://garagehq.deuxfleurs.fr/documentation/", + "https://hub.docker.com/r/waazaafr/garage-s3-tailscale" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Garage stores S3 objects on its configured data volume; capacity scales with stored objects.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 500, + "storage_estimate_min_gb": 20, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "waazaafr/garage-s3-tailscale:latest", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/sysadminsmedia/homebox", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "homebox-sysadminsmedia", + "retire": [ + "homebox" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/sysadminsmedia/homebox", + "https://homebox.software/en/", + "https://homebox.software", + "https://github.com/sysadminsmedia/homebox#readme" + ], + "facts_to_preserve": [ + { + "slug": "homebox", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "083efe63-ed5e-4f8e-a58d-ca000a5a385d", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.763+00:00", + "created_at": "2026-09-02T10:49:43.159579+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/sysadminsmedia/homebox", + "https://homebox.software/en/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "One Homebox household inventory for a few users; item count and image handling raise RAM use.", + "ram_estimate_max_mb": 512, + "ram_estimate_min_mb": 256, + "storage_source_urls": [ + "https://github.com/sysadminsmedia/homebox", + "https://homebox.software/en/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Inventory records, backups, and item photos for a household; photo size and catalog growth drive disk use.", + "ram_estimate_confidence": "medium", + "storage_estimate_max_gb": 10, + "storage_estimate_min_gb": 2, + "storage_estimate_confidence": "medium" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/linuxserver/docker-swag", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "swag-secure-web-application-gateway", + "retire": [ + "swag" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/linuxserver/docker-swag", + "https://docs.linuxserver.io/general/swag", + "http://linuxserver.io/", + "https://docs.linuxserver.io/images/docker-swag/" + ], + "facts_to_preserve": [ + { + "slug": "swag", + "missing_on_keep": { + "compose_url": "https://docs.linuxserver.io/images/docker-swag/" + }, + "retired_ram_estimates": { + "app_id": "579da25c-91bb-4de7-b4d3-b245f927111d", + "ram_note": null, + "checked_at": "2026-09-29T07:27:55.918902+00:00", + "created_at": "2026-09-02T13:57:15.070572+00:00", + "updated_at": "2026-09-29T07:27:55.918902+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://docs.linuxserver.io/images/docker-swag/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "A small reverse proxy and certificate service; enabled add-ons and request load affect memory use.", + "ram_estimate_max_mb": 512, + "ram_estimate_min_mb": 128, + "storage_source_urls": [ + "https://docs.linuxserver.io/images/docker-swag/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "The persistent config folder holds proxy configuration, certificates, and optional fail2ban state.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 10, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "medium" + }, + "retired_docker_support": "official", + "retired_docker_image": "lscr.io/linuxserver/swag:latest", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/privatebin/privatebin", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "privatebin", + "retire": [ + "zerobin" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/PrivateBin/PrivateBin", + "https://privatebin.info/", + "https://github.com/PrivateBin/PrivateBin/wiki", + "https://privatebin.info", + "https://github.com/PrivateBin/PrivateBin#readme" + ], + "facts_to_preserve": [ + { + "slug": "zerobin", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "ae198a88-a930-4d38-a09a-e14fce52ba30", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.77+00:00", + "created_at": "2026-09-02T10:49:43.159579+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/PrivateBin/PrivateBin", + "https://github.com/PrivateBin/PrivateBin/wiki" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "One lightweight PHP paste service with low concurrent traffic.", + "ram_estimate_max_mb": 512, + "ram_estimate_min_mb": 256, + "storage_source_urls": [ + "https://github.com/PrivateBin/PrivateBin", + "https://github.com/PrivateBin/PrivateBin/wiki" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Paste records and configuration in the selected filesystem or database backend; expiry settings limit growth.", + "ram_estimate_confidence": "medium", + "storage_estimate_max_gb": 10, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/odoo/odoo", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "odoo", + "retire": [ + "odoo17", + "libreerp" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/odoo/odoo", + "https://www.odoo.com", + "https://www.odoo.com/documentation/19.0/", + "https://www.odoo.com/", + "https://www.odoo.com/documentation/17.0/", + "https://odoo.com", + "https://www.odoo.com/documentation/16.0/administration.html" + ], + "facts_to_preserve": [ + { + "slug": "odoo17", + "missing_on_keep": { + "compose_url": "https://hub.docker.com/_/odoo" + }, + "retired_ram_estimates": { + "app_id": "d78036b5-56e2-4944-982f-17cdb4b0e897", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T12:13:42.141444+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/odoo/docker" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small single-node service; active users and stored content vary.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 2048, + "storage_source_urls": [ + "https://github.com/odoo/docker" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Small single-node service; active users and stored content vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 20, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "official", + "retired_docker_image": "odoo:17", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + }, + { + "slug": "libreerp", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "d44edbfb-a8c7-47dc-9fcd-41e719bc2bdf", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T10:49:43.159579+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/odoo/odoo" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 256, + "storage_source_urls": [ + "https://github.com/odoo/odoo" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 20, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/pgadmin-org/pgadmin4", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "pgadmin4", + "retire": [ + "pgadmin" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/pgadmin-org/pgadmin4", + "https://www.pgadmin.org/", + "https://www.pgadmin.org/docs/pgadmin4/latest/", + "https://www.pgadmin.org", + "https://github.com/pgadmin-org/pgadmin4#readme" + ], + "facts_to_preserve": [ + { + "slug": "pgadmin", + "missing_on_keep": { + "ram_default_limit_mb": 4096 + }, + "retired_ram_estimates": { + "app_id": "25d851fb-4ea9-4f68-b421-a80c6e9b47a3", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.767+00:00", + "created_at": "2026-09-01T07:06:38.767003+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/pgadmin-org/pgadmin4", + "https://www.pgadmin.org/docs/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "One pgAdmin web service managing a few PostgreSQL servers; concurrent users and query tools raise RAM use.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 512, + "storage_source_urls": [ + "https://github.com/pgadmin-org/pgadmin4", + "https://www.pgadmin.org/docs/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "pgAdmin settings, saved server connections, and logs; managed PostgreSQL database files are stored separately.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 5, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "official", + "retired_docker_image": "dpage/pgadmin4:9.17", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/tvheadend/tvheadend", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "tvheadend", + "retire": [ + "binhex-tvheadend" + ], + "reason": "Community image repackages the same upstream app. Keep the upstream product record.", + "sources": [ + "https://github.com/tvheadend/tvheadend", + "https://tvheadend.org", + "https://github.com/tvheadend/tvheadend#readme", + "https://github.com/binhex/documentation" + ], + "facts_to_preserve": [ + { + "slug": "binhex-tvheadend", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "48e1f9b4-75b1-47e0-9c72-85ac634c93e0", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.758+00:00", + "created_at": "2026-09-02T11:12:41.26667+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/binhex/arch-tvheadend", + "https://github.com/tvheadend/tvheadend" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small TV and IPTV server with a few tuners; streams and guide data affect memory.", + "ram_estimate_max_mb": 4096, + "ram_estimate_min_mb": 1024, + "storage_source_urls": [ + "https://github.com/binhex/arch-tvheadend", + "https://github.com/tvheadend/tvheadend" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Allow for a modest recording library and guide data; recording volume grows with use.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 500, + "storage_estimate_min_gb": 20, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "ghcr.io/binhex/arch-tvheadend", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/etkecc/ketesa", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "ketesa", + "retire": [ + "synapse-admin" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/etkecc/ketesa" + ], + "facts_to_preserve": [ + { + "slug": "synapse-admin", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "478fc737-4b96-4299-afd7-15549ba294b8", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T09:51:47.367629+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/etkecc/ketesa" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 256, + "storage_source_urls": [ + "https://github.com/etkecc/ketesa" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 20, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/woodpecker-ci/woodpecker", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "woodpecker-ci", + "retire": [ + "woodpecker" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/woodpecker-ci/woodpecker", + "https://woodpecker-ci.org", + "https://github.com/woodpecker-ci/woodpecker#readme", + "https://woodpecker-ci.org/docs/administration" + ], + "facts_to_preserve": [ + { + "slug": "woodpecker", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "cbf1ed2b-784e-41f1-b932-5509bd93b122", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.77+00:00", + "created_at": "2026-09-02T09:51:47.367629+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/woodpecker-ci/woodpecker", + "https://woodpecker-ci.org/docs/administration/configuration/server", + "https://woodpecker-ci.org/docs/administration/installation/docker-compose" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small CI server with one agent running a few modest jobs; build steps and concurrency can use far more memory.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 2048, + "storage_source_urls": [ + "https://github.com/woodpecker-ci/woodpecker", + "https://woodpecker-ci.org/docs/administration/configuration/server", + "https://woodpecker-ci.org/docs/administration/installation/docker-compose" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Server database, job logs, and a modest agent workspace or build cache; large artifacts need more.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 100, + "storage_estimate_min_gb": 10, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/kimai/kimai", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "kimai", + "retire": [ + "kimai2" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/kimai/kimai", + "https://www.kimai.org/", + "https://www.kimai.org/documentation/", + "https://www.kimai.org", + "https://github.com/kimai/kimai#readme" + ], + "facts_to_preserve": [ + { + "slug": "kimai2", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "3490e2c9-8f9b-4c29-9cd3-fe745aecd858", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.764+00:00", + "created_at": "2026-09-02T10:49:43.159579+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/kimai/kimai", + "https://www.kimai.org/documentation/installation.html" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small Kimai time-tracking app with a PHP service and SQL database.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 1024, + "storage_source_urls": [ + "https://github.com/kimai/kimai", + "https://www.kimai.org/documentation/installation.html" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Timesheet records are small; invoice exports, attachments, and backups add storage.", + "ram_estimate_confidence": "medium", + "storage_estimate_max_gb": 20, + "storage_estimate_min_gb": 2, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/gethomepage/homepage", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "homepage", + "retire": [ + "homepage-by-gethomepage" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/gethomepage/homepage", + "https://gethomepage.dev", + "https://github.com/gethomepage/homepage#readme" + ], + "facts_to_preserve": [ + { + "slug": "homepage-by-gethomepage", + "missing_on_keep": { + "compose_url": "https://gethomepage.dev/installation/docker/" + }, + "retired_ram_estimates": { + "app_id": "9243dfb4-ce87-41c8-badb-b09358c54788", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.763+00:00", + "created_at": "2026-09-01T07:06:38.767003+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/gethomepage/homepage/blob/dev/docs/installation/docker.md" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small dashboard with service widgets and Docker integrations.", + "ram_estimate_max_mb": 1024, + "ram_estimate_min_mb": 256, + "storage_source_urls": [ + "https://github.com/gethomepage/homepage/blob/dev/docs/installation/docker.md", + "https://github.com/gethomepage/homepage/blob/dev/docs/configs/settings.md" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Configuration files, logs, and a few custom images.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 5, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "official", + "retired_docker_image": "ghcr.io/gethomepage/homepage:latest", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/basecamp/once-campfire", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "campfire", + "retire": [ + "once-campfire" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/basecamp/once-campfire", + "https://once.com/campfire", + "https://github.com/basecamp/once-campfire/blob/main/docs/self-hosting.md", + "https://github.com/basecamp/once-campfire#readme" + ], + "facts_to_preserve": [ + { + "slug": "once-campfire", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "0ccebe76-65ef-4cbe-ba39-53b4b6d60818", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T09:31:18.86036+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/basecamp/once-campfire" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 256, + "storage_source_urls": [ + "https://github.com/basecamp/once-campfire" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 500, + "storage_estimate_min_gb": 50, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/uroni/urbackup_backend", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "urbackup", + "retire": [ + "binhex-urbackup" + ], + "reason": "Community image repackages the same upstream app. Keep the upstream product record.", + "sources": [ + "https://github.com/uroni/urbackup_backend", + "https://github.com/binhex/documentation", + "https://www.urbackup.org/" + ], + "facts_to_preserve": [ + { + "slug": "binhex-urbackup", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "d7b1bbca-bf6e-416a-89ad-8e197b7615f4", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.758+00:00", + "created_at": "2026-09-02T11:12:41.26667+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/binhex/arch-urbackup", + "https://www.urbackup.org/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small backup server managing a few household clients; indexing and concurrent backups affect memory.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 2048, + "storage_source_urls": [ + "https://github.com/binhex/arch-urbackup", + "https://www.urbackup.org/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Backups are the main storage load; capacity follows the protected devices and retention policy.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 2000, + "storage_estimate_min_gb": 100, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "ghcr.io/binhex/arch-urbackup", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/torproject/tor", + "match": "repository", + "classification": "distinct", + "confidence": "high", + "keep": [ + "tor-browser", + "torrelay" + ], + "retire": [], + "reason": "Browser and relay daemon are different products; browser repository appears misassigned and needs correction." + }, + { + "group": "github.com/firefly-iii/data-importer", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "firefly-iii-data-importer", + "retire": [ + "firefly-iii-di" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/firefly-iii/data-importer", + "https://firefly-iii.org/", + "https://docs.firefly-iii.org/how-to/data-importer/", + "https://docs.firefly-iii.org/data-importer/", + "https://docs.firefly-iii.org/data-importer/how-to-use/" + ], + "facts_to_preserve": [ + { + "slug": "firefly-iii-di", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "1b575643-f840-4bba-9125-2ecca773c9b4", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T10:34:27.159229+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/firefly-iii/data-importer" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 256, + "storage_source_urls": [ + "https://github.com/firefly-iii/data-importer" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 20, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/tandoorrecipes/recipes", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "tandoor-recipes", + "retire": [ + "tandoor" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/TandoorRecipes/recipes", + "https://docs.tandoor.dev", + "https://tandoor.dev" + ], + "facts_to_preserve": [ + { + "slug": "tandoor", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "01f179e8-bcf9-436d-88af-dea10a0f75ca", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.769+00:00", + "created_at": "2026-09-02T10:49:43.159579+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/TandoorRecipes/recipes", + "https://docs.tandoor.dev/install/docker/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small Tandoor web app and PostgreSQL deployment with light household usage.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 1024, + "storage_source_urls": [ + "https://github.com/TandoorRecipes/recipes", + "https://docs.tandoor.dev/install/docker/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Recipe database and uploaded food images in the persistent media volume.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 20, + "storage_estimate_min_gb": 5, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/kiwix/kiwix-tools", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "kiwix-serve", + "retire": [ + "kiwix" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/kiwix/kiwix-tools", + "https://download.kiwix.org/release/kiwix-tools", + "https://github.com/kiwix/kiwix-tools#readme", + "https://www.kiwix.org/", + "https://wiki.kiwix.org/wiki/Kiwix-serve/" + ], + "facts_to_preserve": [ + { + "slug": "kiwix", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "42910949-ad00-4bdc-800d-ef0ce0047340", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T10:49:43.159579+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/kiwix/kiwix-tools" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 256, + "storage_source_urls": [ + "https://github.com/kiwix/kiwix-tools" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 20, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/zabbix/zabbix", + "match": "repository", + "classification": "distinct", + "confidence": "high", + "keep": [ + "zabbix-agent", + "zabbix" + ], + "retire": [], + "reason": "Monitoring server and metrics agent are separate components." + }, + { + "group": "github.com/element-hq/synapse", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "synapse", + "retire": [ + "matrix", + "matrix-synapse" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/element-hq/synapse", + "https://element-hq.github.io/synapse", + "https://github.com/element-hq/synapse#readme", + "https://element-hq.github.io/synapse/latest/" + ], + "facts_to_preserve": [ + { + "slug": "matrix", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "f885482b-671e-4895-891b-f1af2a1d29ae", + "ram_note": null, + "checked_at": "2026-09-29T01:59:57.713553+00:00", + "created_at": "2026-09-02T12:06:19.442107+00:00", + "updated_at": "2026-09-29T01:59:57.713553+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://element-hq.github.io/synapse/latest/setup/installation.html" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Active users, federation, and background work affect memory use.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 1024, + "storage_source_urls": [ + "https://element-hq.github.io/synapse/latest/setup/installation.html" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Uploaded media and message history drive storage use.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 500, + "storage_estimate_min_gb": 10, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "official", + "retired_docker_image": "matrixdotorg/synapse:latest", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + }, + { + "slug": "matrix-synapse", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "3bf1fd1e-6927-494e-9ee9-f808f1d60136", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.766+00:00", + "created_at": "2026-09-02T12:06:19.442107+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://element-hq.github.io/synapse/latest/setup/installation.html", + "https://element-hq.github.io/synapse/latest/usage/administration/backups.html" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small Synapse homeserver with a local PostgreSQL database and a few active users.", + "ram_estimate_max_mb": 4096, + "ram_estimate_min_mb": 2048, + "storage_source_urls": [ + "https://element-hq.github.io/synapse/latest/usage/administration/backups.html", + "https://element-hq.github.io/synapse/latest/setup/installation.html" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Small homeserver database and media store for a few users; federation traffic and attachments drive growth.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 100, + "storage_estimate_min_gb": 20, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "matrixdotorg/synapse", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/triliumnext/trilium", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "triliumnext-notes", + "retire": [ + "trilium" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/TriliumNext/Trilium", + "https://triliumnotes.org", + "https://github.com/TriliumNext/Trilium#readme", + "https://github.com/TriliumNext/trilium", + "https://github.com/TriliumNext/trilium/wiki" + ], + "facts_to_preserve": [ + { + "slug": "trilium", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "49161db4-ed47-469a-8555-fd953c1b3fb8", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T09:51:47.367629+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/TriliumNext/trilium" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 256, + "storage_source_urls": [ + "https://github.com/TriliumNext/trilium" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 500, + "storage_estimate_min_gb": 50, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/mumble-voip/mumble", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "mumble", + "retire": [ + "mumbleserver" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/mumble-voip/mumble", + "https://www.mumble.info", + "https://mumble.info", + "https://wiki.mumble.info/wiki/Main_Page" + ], + "facts_to_preserve": [ + { + "slug": "mumbleserver", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "8939b895-3c96-42d9-b2b3-d358f9dbe438", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T10:49:43.159579+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/mumble-voip/mumble" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 256, + "storage_source_urls": [ + "https://github.com/mumble-voip/mumble" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 20, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/chocobozzz/peertube", + "match": "repository", + "classification": "distinct", + "confidence": "high", + "keep": [ + "peertube", + "peertube-remote-runner" + ], + "retire": [], + "reason": "Video service and remote transcoding runner are separately deployed components." + }, + { + "group": "github.com/apache/druid", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "apache-druid", + "retire": [ + "druid" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/apache/druid", + "https://druid.apache.org/", + "https://github.com/apache/druid#readme", + "https://druid.apache.org" + ], + "facts_to_preserve": [ + { + "slug": "druid", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "26733053-8ac9-4e8c-8934-2b0785132a40", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.761+00:00", + "created_at": "2026-09-01T07:06:38.767003+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://druid.apache.org/docs/latest/operations/single-server/", + "https://druid.apache.org/docs/latest/tutorials/docker/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Apache Druid nano-quickstart targets 1 CPU and 4 GiB RAM; the upper end allows room for a small single-server evaluation and metadata services.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 4096, + "storage_source_urls": [ + "https://druid.apache.org/docs/latest/operations/single-server/", + "https://druid.apache.org/docs/latest/tutorials/docker/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "A small single-server installation with initial segment and metadata storage; ingested analytics data and retained segments drive growth.", + "ram_estimate_confidence": "medium", + "storage_estimate_max_gb": 100, + "storage_estimate_min_gb": 20, + "storage_estimate_confidence": "medium" + }, + "retired_docker_support": "official", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/standardnotes/app", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "standard-notes", + "retire": [ + "snweb", + "standardnotes-web" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/standardnotes/app", + "https://standardnotes.com", + "https://docs.standardnotes.com/self-hosting/getting-started", + "https://standardnotes.org/", + "https://docs.standardnotes.org/", + "https://forums.unraid.net/topic/88410-support-xthursdayx-unraid-docker-templates/" + ], + "facts_to_preserve": [ + { + "slug": "snweb", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "fe4d03f6-9050-4ef5-81f8-4932bcd59e06", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T10:49:43.159579+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/standardnotes/app" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 256, + "storage_source_urls": [ + "https://github.com/standardnotes/app" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 500, + "storage_estimate_min_gb": 50, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + }, + { + "slug": "standardnotes-web", + "missing_on_keep": { + "docker_image": "standardnotes/web:stable" + }, + "retired_ram_estimates": { + "app_id": "170d0f79-5cf1-45cc-8739-c186315331b1", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T12:49:49.261175+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://standardnotes.org/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 2048, + "storage_source_urls": [ + "https://standardnotes.org/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 20, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "standardnotes/web:stable", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/passteque/gluetun", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "gluetunvpn", + "retire": [ + "gluetun-full" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/passteque/gluetun", + "https://forums.unraid.net/topic/111725-support-diamondprecisioncomputing-all-images-and-files", + "https://github.com/passteque/gluetun/blob/master/README.md" + ], + "facts_to_preserve": [ + { + "slug": "gluetun-full", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "b4e758c5-b376-4b87-86b4-837714b5ca9e", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T11:46:23.046796+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/passteque/gluetun" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small single-node service; active users and stored content vary.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 2048, + "storage_source_urls": [ + "https://github.com/passteque/gluetun" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Small single-node service; active users and stored content vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 20, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "qmcgaw/gluetun:latest", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/shopware/shopware", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "shopware-community-edition", + "retire": [ + "shopware" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/shopware/shopware", + "https://shopware.com", + "https://github.com/shopware/shopware#readme", + "https://www.shopware.com/en/community/community-edition/" + ], + "facts_to_preserve": [ + { + "slug": "shopware", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "395790ad-8be8-4140-a3c1-c8a6a286cd80", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T09:25:32.635959+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/shopware/shopware" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 256, + "storage_source_urls": [ + "https://github.com/shopware/shopware" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 20, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/apache/answer", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "apache-answer", + "retire": [ + "answer" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/apache/answer", + "https://answer.apache.org", + "https://github.com/apache/answer#readme", + "https://answer.apache.org/docs/" + ], + "facts_to_preserve": [ + { + "slug": "answer", + "missing_on_keep": { + "docker_url": "https://github.com/apache/answer/blob/HEAD/docker-compose.yaml" + }, + "retired_ram_estimates": { + "app_id": "ff803085-24b3-41ef-a9e9-ae16ee8b6e27", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.757+00:00", + "created_at": "2026-09-01T07:06:38.767003+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://answer.apache.org/docs/installation/", + "https://github.com/apache/answer" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "One small Apache Answer community with light traffic and its database service; concurrent visits and image uploads increase use.", + "ram_estimate_max_mb": 1024, + "ram_estimate_min_mb": 512, + "storage_source_urls": [ + "https://answer.apache.org/docs/installation/", + "https://github.com/apache/answer" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "App state, database, logs, and a small set of community uploads; user images drive growth.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 10, + "storage_estimate_min_gb": 2, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "official", + "retired_docker_image": "apache/answer", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/mealie-recipes/mealie", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "mealie", + "retire": [ + "mealiev1" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/mealie-recipes/mealie", + "https://docs.mealie.io", + "https://mealie.io/", + "https://forums.unraid.net/topic/138058-support-catduck-templates/" + ], + "facts_to_preserve": [ + { + "slug": "mealiev1", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "4b0ac52c-15c1-4752-94fc-9760090142b4", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T12:06:19.442107+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://mealie.io/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small single-node service; data volume and request load vary.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 2048, + "storage_source_urls": [ + "https://mealie.io/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Small single-node service; data volume and request load vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 100, + "storage_estimate_min_gb": 5, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "ghcr.io/mealie-recipes/mealie:nightly", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/karanhudia/borg-ui", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "borg-ui", + "retire": [ + "borg-web-ui" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/karanhudia/borg-ui", + "https://karanhudia.github.io/borg-ui/", + "https://karanhudia.github.io/borg-ui/configuration", + "https://docs.borgui.com" + ], + "facts_to_preserve": [ + { + "slug": "borg-web-ui", + "missing_on_keep": { + "docker_image": "ainullcode/borg-ui:latest", + "categories": [ + "automation" + ] + }, + "retired_ram_estimates": { + "app_id": "20911d6d-8692-4ff4-97d5-b2108a2141a3", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.759+00:00", + "created_at": "2026-09-02T11:12:41.26667+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/karanhudia/borg-ui", + "https://docs.borgui.com/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "One Borg UI manages scheduled backup and restore jobs; the repository archives, not the UI itself, drive disk use.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 512, + "storage_source_urls": [ + "https://github.com/karanhudia/borg-ui", + "https://docs.borgui.com/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "One Borg UI manages scheduled backup and restore jobs; the repository archives, not the UI itself, drive disk use.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 500, + "storage_estimate_min_gb": 20, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "ainullcode/borg-ui:latest", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/handbrake/handbrake", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "handbrake", + "retire": [ + "handbrake-for-nvidia" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/HandBrake/HandBrake", + "https://handbrake.fr/", + "https://handbrake.fr/docs/en/latest/", + "https://forums.unraid.net/topic/57420-support-handbrake/" + ], + "facts_to_preserve": [ + { + "slug": "handbrake-for-nvidia", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "5c7b6e97-d3a3-4804-8bca-784cc716f07f", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T11:48:15.3369+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://handbrake.fr/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small single-node service; active users and stored content vary.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 2048, + "storage_source_urls": [ + "https://handbrake.fr/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Small single-node service; active users and stored content vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 20, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "zocker160/handbrake-nvenc:latest", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/jenkinsci/jenkins", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "jenkins", + "retire": [ + "binhex-jenkins" + ], + "reason": "Community image repackages the same upstream app. Keep the upstream product record.", + "sources": [ + "https://github.com/jenkinsci/jenkins", + "https://github.com/binhex/documentation", + "https://www.jenkins.io/", + "https://www.jenkins.io/doc/" + ], + "facts_to_preserve": [ + { + "slug": "binhex-jenkins", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "ae883a85-d43e-417b-9fb0-0212715ea050", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.758+00:00", + "created_at": "2026-09-02T11:12:41.26667+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/binhex/arch-jenkins", + "https://www.jenkins.io/doc/book/installing/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "A small Jenkins controller with a few lightweight jobs and a limited number of concurrent builds.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 2048, + "storage_source_urls": [ + "https://github.com/binhex/arch-jenkins", + "https://www.jenkins.io/doc/book/installing/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Jenkins home, plugins, workspaces, build logs, and a modest set of retained artifacts.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 100, + "storage_estimate_min_gb": 20, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "ghcr.io/binhex/arch-jenkins", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/lobehub/lobehub", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "lobehub", + "retire": [ + "lobechat" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/lobehub/lobehub", + "https://lobehub.com/", + "https://lobehub.com/docs/", + "https://github.com/lobehub/lobehub#readme" + ], + "facts_to_preserve": [ + { + "slug": "lobechat", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "18df83ac-c726-4ea1-a16e-35cdaddf2a56", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T09:25:32.635959+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/lobehub/lobehub" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 2048, + "storage_source_urls": [ + "https://github.com/lobehub/lobehub" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 20, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/phpservermon/phpservermon", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "phpservermonitor", + "retire": [ + "phpservermon" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/phpservermon/phpservermon", + "https://www.phpservermonitor.org/", + "https://docs.phpservermonitor.org/en/latest/", + "https://forums.unraid.net/topic/104557-support-php-server-monitor-corneliousjd-repo/" + ], + "facts_to_preserve": [ + { + "slug": "phpservermon", + "missing_on_keep": { + "website_url": "https://www.phpservermonitor.org/" + }, + "retired_ram_estimates": { + "app_id": "0e0d1522-9135-493c-afd2-21adbeebefb2", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T10:49:43.159579+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/phpservermon/phpservermon" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 256, + "storage_source_urls": [ + "https://github.com/phpservermon/phpservermon" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 20, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "github.com/simstudioai/sim", + "match": "repository", + "classification": "duplicate", + "confidence": "high", + "keep": "sim-studio", + "retire": [ + "simstudio-app" + ], + "reason": "Same product purpose and upstream identity; alternate name, old name, release or deployment variant. Keep the upstream product record.", + "sources": [ + "https://github.com/simstudioai/sim", + "https://www.sim.ai/", + "https://docs.sim.ai/introduction", + "https://simstudio.ai/", + "https://github.com/simstudioai/sim/issues" + ], + "facts_to_preserve": [ + { + "slug": "simstudio-app", + "missing_on_keep": { + "docker_image": "ghcr.io/simstudioai/simstudio:latest", + "categories": [ + "developer-tools" + ] + }, + "retired_ram_estimates": { + "app_id": "fa36bdfd-a4ec-43fe-8270-5b75847154c5", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T12:45:41.214707+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://simstudio.ai/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small single-node service; data volume and request load vary.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 2048, + "storage_source_urls": [ + "https://simstudio.ai/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Small single-node service; data volume and request load vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 100, + "storage_estimate_min_gb": 5, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "ghcr.io/simstudioai/simstudio:latest", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "snipe-it / snipeit", + "match": "name_or_website", + "classification": "duplicate", + "confidence": "high", + "keep": "snipe-it", + "retire": [ + "snipeit" + ], + "reason": "Same upstream product; old repository, container packaging, or missing repository. Website and reviewed product purpose agree.", + "sources": [ + "https://github.com/grokability/snipe-it", + "https://snipeitapp.com", + "https://github.com/grokability/snipe-it#readme", + "https://github.com/snipe/snipe-it", + "https://snipe-it.readme.io/docs/introduction" + ], + "facts_to_preserve": [ + { + "slug": "snipeit", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "7b2a2d24-a1da-4f25-90d7-0cc1585fa87c", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T10:49:43.159579+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/snipe/snipe-it" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_max_mb": 2048, + "ram_estimate_min_mb": 256, + "storage_source_urls": [ + "https://github.com/snipe/snipe-it" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 20, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "tandoor-recipes / tandoorrecipes", + "match": "name_or_website", + "classification": "duplicate", + "confidence": "high", + "keep": "tandoor-recipes", + "retire": [ + "tandoorrecipes" + ], + "reason": "Same upstream product; old repository, container packaging, or missing repository. Website and reviewed product purpose agree.", + "sources": [ + "https://github.com/TandoorRecipes/recipes", + "https://docs.tandoor.dev", + "https://github.com/vabene1111/recipes", + "https://forums.unraid.net/topic/98179-support-recipes-corneliousjd-repo/" + ], + "facts_to_preserve": [ + { + "slug": "tandoorrecipes", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "5ec308fd-43b2-4ed4-b034-e87858cce52f", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.769+00:00", + "created_at": "2026-09-02T12:49:49.261175+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://docs.tandoor.dev/install/docker/", + "https://docs.tandoor.dev/system/configuration/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small household recipe server with PostgreSQL and a modest collection of recipe images and documents.", + "ram_estimate_max_mb": 1024, + "ram_estimate_min_mb": 512, + "storage_source_urls": [ + "https://docs.tandoor.dev/install/docker/", + "https://docs.tandoor.dev/system/configuration/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Small household recipe server with PostgreSQL and a modest collection of recipe images and documents.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 10, + "storage_estimate_min_gb": 2, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "vabene1111/recipes", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "foundry-virtual-tabletop / foundry", + "match": "name_or_website", + "classification": "duplicate", + "confidence": "high", + "keep": "foundry-virtual-tabletop", + "retire": [ + "foundry" + ], + "reason": "Same upstream product; old repository, container packaging, or missing repository. Website and reviewed product purpose agree.", + "sources": [ + "https://github.com/foundryvtt/foundryvtt", + "https://foundryvtt.com", + "https://foundryvtt.com/kb/", + "https://foundryvtt.com/", + "https://foundryvtt.com/article/installation/" + ], + "facts_to_preserve": [ + { + "slug": "foundry", + "missing_on_keep": { + "docker_url": "https://hub.docker.com/r/felddy/foundryvtt", + "docker_image": "felddy/foundryvtt:release", + "compose_url": "https://github.com/felddy/foundryvtt-docker", + "docker_support": "community" + }, + "retired_ram_estimates": { + "app_id": "fd4e814c-0503-4c1e-9d32-b7a8c9edc5fd", + "ram_note": null, + "checked_at": "2026-09-28T22:40:58.765367+00:00", + "created_at": "2026-09-02T11:43:52.951316+00:00", + "updated_at": "2026-09-28T22:40:58.765367+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://foundryvtt.com/article/requirements/", + "https://github.com/felddy/foundryvtt-docker" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Estimated from Foundry dedicated-server minimum of 2 GB and 4 GB recommended; world content and modules increase use.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 2048, + "storage_source_urls": [ + "https://foundryvtt.com/article/requirements/", + "https://foundryvtt.com/article/installation/", + "https://github.com/felddy/foundryvtt-docker" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Estimated from the 1 GB server minimum plus worlds, modules, and game assets.", + "ram_estimate_confidence": "medium", + "storage_estimate_max_gb": 100, + "storage_estimate_min_gb": 1, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "felddy/foundryvtt:release", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "tiny-media-manager / tinymediamanager", + "match": "name_or_website", + "classification": "duplicate", + "confidence": "high", + "keep": "tiny-media-manager", + "retire": [ + "tinymediamanager" + ], + "reason": "Same upstream product; old repository, container packaging, or missing repository. Website and reviewed product purpose agree.", + "sources": [ + "https://www.tinymediamanager.org/", + "https://apps.truenas.com/catalog/tiny-media-manager_community/", + "https://github.com/jlesage/docker-tinymediamanager", + "https://github.com/jlesage/docker-tinymediamanager#readme" + ], + "facts_to_preserve": [ + { + "slug": "tinymediamanager", + "missing_on_keep": { + "docker_image": "jlesage/tinymediamanager", + "docker_support": "community" + }, + "retired_ram_estimates": { + "app_id": "5b78bac8-bc56-4db7-a35e-96ca7d6db10a", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T12:53:37.949678+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/jlesage/docker-tinymediamanager" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 2048, + "storage_source_urls": [ + "https://github.com/jlesage/docker-tinymediamanager" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Typical small self-hosted instance; workload and stored data vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 500, + "storage_estimate_min_gb": 50, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "jlesage/tinymediamanager", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "musicbrainz-picard / musicbrainzpicard", + "match": "name_or_website", + "classification": "duplicate", + "confidence": "high", + "keep": "musicbrainz-picard", + "retire": [ + "musicbrainzpicard" + ], + "reason": "Same upstream product; old repository, container packaging, or missing repository. Website and reviewed product purpose agree.", + "sources": [ + "https://github.com/metabrainz/picard", + "https://picard.musicbrainz.org/", + "https://picard-docs.musicbrainz.org/en/latest/", + "https://github.com/jlesage/docker-musicbrainz-picard", + "https://github.com/jlesage/docker-musicbrainz-picard#readme" + ], + "facts_to_preserve": [ + { + "slug": "musicbrainzpicard", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "f411c70e-be34-4cfe-ad6f-75ff0ac1acfe", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T12:10:05.792277+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/jlesage/docker-musicbrainz-picard" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small single-node service; data volume and request load vary.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 2048, + "storage_source_urls": [ + "https://github.com/jlesage/docker-musicbrainz-picard" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Small single-node service; data volume and request load vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 100, + "storage_estimate_min_gb": 5, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "jlesage/musicbrainz-picard", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "sqlite-browser / sqlitebrowser", + "match": "name_or_website", + "classification": "duplicate", + "confidence": "high", + "keep": "sqlite-browser", + "retire": [ + "sqlitebrowser" + ], + "reason": "Same upstream product; old repository, container packaging, or missing repository. Website and reviewed product purpose agree.", + "sources": [ + "https://github.com/sqlitebrowser/sqlitebrowser", + "https://sqlitebrowser.org/", + "https://github.com/sqlitebrowser/sqlitebrowser/issues", + "https://github.com/linuxserver/docker-sqlitebrowser", + "https://github.com/linuxserver/docker-sqlitebrowser#readme" + ], + "facts_to_preserve": [ + { + "slug": "sqlitebrowser", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "8f8634ce-a9d3-493e-8e7d-1bb7d84e43eb", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T12:49:49.261175+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/linuxserver/docker-sqlitebrowser" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small single-node service; data volume and request load vary.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 1024, + "storage_source_urls": [ + "https://github.com/linuxserver/docker-sqlitebrowser" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Small single-node service; data volume and request load vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 100, + "storage_estimate_min_gb": 5, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "lscr.io/linuxserver/sqlitebrowser", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "one-time-secret / onetimesecret", + "match": "name_or_website", + "classification": "duplicate", + "confidence": "high", + "keep": "one-time-secret", + "retire": [ + "onetimesecret" + ], + "reason": "Same upstream product; old repository, container packaging, or missing repository. Website and reviewed product purpose agree.", + "sources": [ + "https://github.com/onetimesecret/onetimesecret", + "https://uk.onetimesecret.com", + "https://docs.onetimesecret.com", + "https://github.com/siw36/onetimesecret", + "https://forums.unraid.net/topic/104402-support-onetimesecret-corneliousjd-repo/" + ], + "facts_to_preserve": [ + { + "slug": "onetimesecret", + "missing_on_keep": { + "docker_image": "siw36/onetimesecret" + }, + "retired_ram_estimates": { + "app_id": "2fe5e301-b6df-4779-a16e-fcdb7cdb60a3", + "ram_note": null, + "checked_at": "2026-09-28T11:08:23.499769+00:00", + "created_at": "2026-09-02T12:13:42.141444+00:00", + "updated_at": "2026-09-28T11:08:23.499769+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/siw36/onetimesecret" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Small single-node service; data volume and request load vary.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 2048, + "storage_source_urls": [ + "https://github.com/siw36/onetimesecret" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Small single-node service; data volume and request load vary.", + "ram_estimate_confidence": "low", + "storage_estimate_max_gb": 100, + "storage_estimate_min_gb": 5, + "storage_estimate_confidence": "low" + }, + "retired_docker_support": "community", + "retired_docker_image": "siw36/onetimesecret", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "jan-ai / jan", + "match": "name_or_website", + "classification": "duplicate", + "confidence": "high", + "keep": "jan-ai", + "retire": [ + "jan" + ], + "reason": "Same upstream product; old repository, container packaging, or missing repository. Website and reviewed product purpose agree.", + "sources": [ + "https://github.com/menloresearch/jan", + "https://jan.ai/", + "https://github.com/menloresearch/jan#readme", + "https://github.com/janhq/jan", + "https://www.jan.ai/docs" + ], + "facts_to_preserve": [ + { + "slug": "jan", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "c8d6106d-57be-4338-9ad2-caf146037cd8", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.764+00:00", + "created_at": "2026-09-01T07:06:38.767003+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://www.jan.ai/docs/desktop/windows", + "https://www.jan.ai/docs/desktop/cli" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "One Jan desktop/API instance using a small local model; official Windows requirements map 8 GB to models up to 3B and 16 GB to models up to 7B.", + "ram_estimate_max_mb": 16384, + "ram_estimate_min_mb": 8192, + "storage_source_urls": [ + "https://www.jan.ai/docs/desktop/windows", + "https://www.jan.ai/docs/desktop/cli" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Jan application data and one or a few locally downloaded models; official minimum is 10 GB and additional models increase usage.", + "ram_estimate_confidence": "medium", + "storage_estimate_max_gb": 50, + "storage_estimate_min_gb": 10, + "storage_estimate_confidence": "medium" + }, + "retired_docker_support": "none", + "retired_docker_image": null, + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "nextcloud / nextcloud-aio-mastercontainer", + "match": "name_or_website", + "classification": "duplicate", + "confidence": "high", + "keep": "nextcloud", + "retire": [ + "nextcloud-aio-mastercontainer" + ], + "reason": "Same upstream product; old repository, container packaging, or missing repository. Website and reviewed product purpose agree.", + "sources": [ + "https://github.com/nextcloud/server", + "https://nextcloud.com", + "https://github.com/nextcloud/server#readme", + "https://github.com/nextcloud/all-in-one", + "https://nextcloud.com/" + ], + "facts_to_preserve": [ + { + "slug": "nextcloud-aio-mastercontainer", + "missing_on_keep": { + "docker_url": "https://github.com/nextcloud-releases/all-in-one/pkgs/container/all-in-one", + "categories": [ + "system-admin-hosting" + ] + }, + "retired_ram_estimates": { + "app_id": "20be1689-4792-4cfc-a81d-f7070be2d9a9", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.767+00:00", + "created_at": "2026-09-02T12:10:05.792277+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://github.com/nextcloud/all-in-one", + "https://github.com/nextcloud/all-in-one/discussions/1335" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Minimal Nextcloud AIO stack for a few users with a small file set; official minimum is 2 GB RAM and 40 GB system storage. Estimated RAM for the stated small workload.", + "ram_estimate_max_mb": 8192, + "ram_estimate_min_mb": 4096, + "storage_source_urls": [ + "https://github.com/nextcloud/all-in-one", + "https://github.com/nextcloud/all-in-one/discussions/1335" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Minimal Nextcloud AIO stack for a few users with a small file set; official minimum is 2 GB RAM and 40 GB system storage. Estimated storage for the stated small workload.", + "ram_estimate_confidence": "medium", + "storage_estimate_max_gb": 100, + "storage_estimate_min_gb": 40, + "storage_estimate_confidence": "medium" + }, + "retired_docker_support": "official", + "retired_docker_image": "ghcr.io/nextcloud-releases/all-in-one:latest", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "sentry / sentry-self-hosted", + "match": "name_or_website", + "classification": "duplicate", + "confidence": "high", + "keep": "sentry", + "retire": [ + "sentry-self-hosted" + ], + "reason": "Same upstream product; old repository, container packaging, or missing repository. Website and reviewed product purpose agree.", + "sources": [ + "https://github.com/getsentry/sentry", + "https://sentry.io", + "https://develop.sentry.dev/self-hosted/", + "https://github.com/getsentry/self-hosted", + "https://sentry.io/", + "https://docs.sentry.io/" + ], + "facts_to_preserve": [ + { + "slug": "sentry-self-hosted", + "missing_on_keep": { + "docker_image": "sentry-cleanup-self-hosted-local" + }, + "retired_ram_estimates": { + "app_id": "ca1a3630-6976-4385-a501-1c38a2912fba", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.768+00:00", + "created_at": "2026-09-01T07:06:38.767003+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://develop.sentry.dev/self-hosted/" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Official self-hosted Compose stack lists 4 GB minimum and recommends 16 GB RAM; this covers a small low-volume instance.", + "ram_estimate_max_mb": 16384, + "ram_estimate_min_mb": 4096, + "storage_source_urls": [ + "https://develop.sentry.dev/self-hosted/" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Official guidance lists 20 GB free disk; the estimate allows extra space for a small retained event history.", + "ram_estimate_confidence": "medium", + "storage_estimate_max_gb": 100, + "storage_estimate_min_gb": 20, + "storage_estimate_confidence": "medium" + }, + "retired_docker_support": "official", + "retired_docker_image": "sentry-cleanup-self-hosted-local", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "collabora-code / collabora", + "match": "name_or_website", + "classification": "duplicate", + "confidence": "high", + "keep": "collabora-code", + "retire": [ + "collabora" + ], + "reason": "Same upstream product; old repository, container packaging, or missing repository. Website and reviewed product purpose agree.", + "sources": [ + "https://github.com/CollaboraOnline/online.mirror", + "https://www.collaboraonline.com/code/", + "https://sdk.collaboraonline.com/docs/", + "https://gerrit.collaboraoffice.com/core" + ], + "facts_to_preserve": [ + { + "slug": "collabora", + "missing_on_keep": {}, + "retired_ram_estimates": { + "app_id": "cd050d78-68fe-4196-84d5-3aa7f318d24b", + "ram_note": null, + "checked_at": "2026-10-01T03:17:58.76+00:00", + "created_at": "2026-09-02T05:18:16.798279+00:00", + "updated_at": "2026-10-01T05:14:26.778201+00:00", + "storage_note": null, + "ram_source_urls": [ + "https://www.collaboraonline.com/code/", + "https://github.com/CollaboraOnline/online.mirror", + "https://sdk.collaboraonline.com/docs/installation/CODE_Docker_image.html" + ], + "ram_estimate_kind": "estimated", + "ram_estimate_basis": "Collabora Online has a roughly 2 GB base allowance plus memory for active document workers.", + "ram_estimate_max_mb": 4096, + "ram_estimate_min_mb": 2048, + "storage_source_urls": [ + "https://www.collaboraonline.com/code/", + "https://github.com/CollaboraOnline/online.mirror", + "https://sdk.collaboraonline.com/docs/installation/CODE_Docker_image.html" + ], + "storage_estimate_kind": "estimated", + "storage_estimate_basis": "Container image, logs and temporary document work; the connected WOPI host stores original files.", + "ram_estimate_confidence": "medium", + "storage_estimate_max_gb": 10, + "storage_estimate_min_gb": 2, + "storage_estimate_confidence": "medium" + }, + "retired_docker_support": "official", + "retired_docker_image": "collabora/code:latest", + "handling": "All linked evidence is merged into data_source_urls. Structured facts remain on the unpublished row; deployment-specific RAM, Docker and category differences are not copied without separate factual review." + } + ] + }, + { + "group": "zennotes", + "match": "name_or_website", + "classification": "distinct", + "confidence": "high", + "keep": [ + "zennotes", + "zen-notes" + ], + "retire": [], + "reason": "Different independent note projects." + }, + { + "group": "torrelay", + "match": "name_or_website", + "classification": "unsure", + "confidence": "low", + "keep": [ + "tor-relay", + "torrelay" + ], + "retire": [], + "reason": "Both relay deployments, but independent container repository may add distinct management behavior." + }, + { + "group": "arksurvivalascended", + "match": "name_or_website", + "classification": "unsure", + "confidence": "low", + "keep": [ + "ark-survival-ascended", + "ark-survivalascended" + ], + "retire": [], + "reason": "Same game, but independently maintained server packages may expose different functions." + }, + { + "group": "cap", + "match": "name_or_website", + "classification": "distinct", + "confidence": "high", + "keep": [ + "cap", + "cap-captcha" + ], + "retire": [], + "reason": "Screen recorder and proof-of-work CAPTCHA are unrelated." + }, + { + "group": "unraiddeck", + "match": "name_or_website", + "classification": "distinct", + "confidence": "high", + "keep": [ + "unraiddeck", + "unraid-deck" + ], + "retire": [], + "reason": "Server dashboard and mobile push notification agent are different products." + }, + { + "group": "openpanel", + "match": "name_or_website", + "classification": "distinct", + "confidence": "high", + "keep": [ + "openpanel", + "openpanel-analytics" + ], + "retire": [], + "reason": "Hosting control panel and analytics service are unrelated." + }, + { + "group": "nextexplorer", + "match": "name_or_website", + "classification": "distinct", + "confidence": "high", + "keep": [ + "next-explorer", + "nextexplorer" + ], + "retire": [], + "reason": "Different maintainers and codebases; shared generic name." + }, + { + "group": "selfhostedgateway", + "match": "name_or_website", + "classification": "unsure", + "confidence": "low", + "keep": [ + "selfhosted-gateway", + "self-hosted-gateway" + ], + "retire": [], + "reason": "Missing source on one record; confirm gateway identity before retiring." + }, + { + "group": "filedrop", + "match": "name_or_website", + "classification": "distinct", + "confidence": "high", + "keep": [ + "file-drop", + "filedrop" + ], + "retire": [], + "reason": "Independent file-sharing implementations with different repositories." + }, + { + "group": "youtubedlmaterial", + "match": "name_or_website", + "classification": "unsure", + "confidence": "low", + "keep": [ + "youtubedl-material", + "youtube-dl-material" + ], + "retire": [], + "reason": "voc0der maintains a fork; check whether it is a separately maintained successor before merging." + }, + { + "group": "youtubedlserver", + "match": "name_or_website", + "classification": "unsure", + "confidence": "low", + "keep": [ + "youtubedl-server", + "youtube-dl-server" + ], + "retire": [], + "reason": "Independent fork lineage; shared name is insufficient." + }, + { + "group": "ko-fi.com/diogovalentte", + "match": "name_or_website", + "classification": "distinct", + "confidence": "high", + "keep": [ + "homarr-iframes", + "mantium" + ], + "retire": [], + "reason": "Different product roles or independent apps share a publisher, sponsor, documentation or generic homepage; keep all." + }, + { + "group": "nextcloud.com/talk", + "match": "name_or_website", + "classification": "distinct", + "confidence": "high", + "keep": [ + "nextcloud-talk", + "nextcloud-signaling" + ], + "retire": [], + "reason": "Different product roles or independent apps share a publisher, sponsor, documentation or generic homepage; keep all." + }, + { + "group": "openg2p.org", + "match": "name_or_website", + "classification": "unsure", + "confidence": "low", + "keep": [ + "openg2p-selfservice-api", + "openg2p-spar-self-service-api", + "openg2p-selfservice-ui", + "openg2p-spar-self-service-ui-lk", + "openg2p-spar-self-service-ui" + ], + "retire": [], + "reason": "API duplicate is handled by repository group; UI, SPAR UI and country-specific UI may be separate deployments." + }, + { + "group": "docker.nasqueron.org", + "match": "name_or_website", + "classification": "distinct", + "confidence": "high", + "keep": [ + "auth-grove", + "orbeon", + "vimbadmin", + "tommy", + "mysql", + "phabricator" + ], + "retire": [], + "reason": "Different product roles or independent apps share a publisher, sponsor, documentation or generic homepage; keep all." + }, + { + "group": "uncloud.run", + "match": "name_or_website", + "classification": "distinct", + "confidence": "high", + "keep": [ + "uncloud", + "unregistry" + ], + "retire": [], + "reason": "Different product roles or independent apps share a publisher, sponsor, documentation or generic homepage; keep all." + }, + { + "group": "linuxserver.io", + "match": "name_or_website", + "classification": "distinct", + "confidence": "high", + "keep": [ + "booksonic", + "hydra", + "mylar", + "openssh-server", + "sickrage", + "swag" + ], + "retire": [], + "reason": "Different product roles or independent apps share a publisher, sponsor, documentation or generic homepage; keep all." + }, + { + "group": "firefly-iii.org", + "match": "name_or_website", + "classification": "distinct", + "confidence": "high", + "keep": [ + "firefly-iii", + "firefly-iii-data-importer" + ], + "retire": [], + "reason": "Different product roles or independent apps share a publisher, sponsor, documentation or generic homepage; keep all." + }, + { + "group": "simplex.chat", + "match": "name_or_website", + "classification": "distinct", + "confidence": "high", + "keep": [ + "simplex", + "simplex-chat" + ], + "retire": [], + "reason": "Different product roles or independent apps share a publisher, sponsor, documentation or generic homepage; keep all." + }, + { + "group": "torproject.org", + "match": "name_or_website", + "classification": "distinct", + "confidence": "high", + "keep": [ + "obfs4-bridge", + "tor-browser", + "torrelay" + ], + "retire": [], + "reason": "Different product roles or independent apps share a publisher, sponsor, documentation or generic homepage; keep all." + }, + { + "group": "memories.gallery", + "match": "name_or_website", + "classification": "distinct", + "confidence": "high", + "keep": [ + "nextcloud-memories", + "go-vod" + ], + "retire": [], + "reason": "Different product roles or independent apps share a publisher, sponsor, documentation or generic homepage; keep all." + }, + { + "group": "coder.com", + "match": "name_or_website", + "classification": "distinct", + "confidence": "high", + "keep": [ + "code-server", + "coder" + ], + "retire": [], + "reason": "Different product roles or independent apps share a publisher, sponsor, documentation or generic homepage; keep all." + }, + { + "group": "forgejo.org", + "match": "name_or_website", + "classification": "distinct", + "confidence": "high", + "keep": [ + "forgejo-runner", + "forgejo" + ], + "retire": [], + "reason": "Different product roles or independent apps share a publisher, sponsor, documentation or generic homepage; keep all." + }, + { + "group": "min.io", + "match": "name_or_website", + "classification": "distinct", + "confidence": "high", + "keep": [ + "aistor", + "minio" + ], + "retire": [], + "reason": "Different product roles or independent apps share a publisher, sponsor, documentation or generic homepage; keep all." + }, + { + "group": "etherpad.org", + "match": "name_or_website", + "classification": "unsure", + "confidence": "low", + "keep": [ + "etherpad", + "etherpad-mypads" + ], + "retire": [], + "reason": "MyPads plugin and Etherpad core may represent different packaged functionality." + }, + { + "group": "standardnotes.com", + "match": "name_or_website", + "classification": "distinct", + "confidence": "high", + "keep": [ + "standard-notes", + "snserver" + ], + "retire": [], + "reason": "Different product roles or independent apps share a publisher, sponsor, documentation or generic homepage; keep all." + }, + { + "group": "prometheus.io", + "match": "name_or_website", + "classification": "distinct", + "confidence": "high", + "keep": [ + "prometheus", + "prom-alertmanager" + ], + "retire": [], + "reason": "Different product roles or independent apps share a publisher, sponsor, documentation or generic homepage; keep all." + }, + { + "group": "en.wikipedia.org/wiki/Reverse_proxy", + "match": "name_or_website", + "classification": "distinct", + "confidence": "high", + "keep": [ + "redirect", + "reverseproxy" + ], + "retire": [], + "reason": "Different product roles or independent apps share a publisher, sponsor, documentation or generic homepage; keep all." + }, + { + "group": "wireguard.com", + "match": "name_or_website", + "classification": "distinct", + "confidence": "high", + "keep": [ + "wireguard", + "wireguard-client" + ], + "retire": [], + "reason": "Different product roles or independent apps share a publisher, sponsor, documentation or generic homepage; keep all." + } + ] +} diff --git a/scripts/self-host-apps/ovh/README.md b/scripts/self-host-apps/ovh/README.md index 507034882..4d4485a53 100644 --- a/scripts/self-host-apps/ovh/README.md +++ b/scripts/self-host-apps/ovh/README.md @@ -1,6 +1,6 @@ # Self-host app cron jobs on OVH -These seven data jobs and one catch-up job run as `ubuntu` on OVH. GitHub no +These eight data jobs and one catch-up job run as `ubuntu` on OVH. GitHub no longer schedules them. Cron is already enabled on the server. No new daemon or timer is required. The wrapper uses a transient systemd resource scope through ubuntu's enabled user manager (`loginctl enable-linger ubuntu`) to cap the complete process tree at 2 GB RAM, @@ -61,6 +61,13 @@ Node, never evaluated as shell code. Credentials: `sslmode=disable`; use the same connection as launch-directory metrics. - `GITHUB_TOKEN`: GitHub discovery, catalogs, facts and technical jobs. Copy an existing authorised Infisical credential; no interactive `gh` login is needed. +- `AWESOME_LIST_GITHUB_TOKEN`: required only for `self-host-awesome-list`. Add it + to Infisical `stack127 /vps/self-host-apps`. Use a fine-grained token with + resource owner `stack127labs`, repository access only to + `awesome-self-hosted-apps`, and Contents read/write (Metadata read is automatic). + The existing `GITHUB_TOKEN` is for discovery/API reads; its write access is not + assumed. Git uses a temporary askpass script that reads the token from the + environment; no token is stored in a remote URL or Git configuration. - `GITLAB_TOKEN`, `CODEBERG_TOKEN`: optional, for source-host refresh. Without them the job reads the public APIs anonymously, as it always did on GitHub Actions. Add them only if rate limits start to fail runs. Never fill them with placeholders. @@ -86,12 +93,13 @@ by dedupe even on failure. No failed catalog is silently treated as success. | refresh-github | Daily 05:30, 17:30 | Daily 07:06, 19:06 | 20 min | | refresh-source-hosts | Daily 11:06 | Daily 11:22 | 30 min | | refresh-technical | Monday 10:53 | Monday 12:26 | 60 min | +| awesome-list | New job | Monday 14:02 | 20 min | Each data job has its own cron line at the nominal times above, with `--due`. The dashboard therefore sees the real schedule and can flag overdue work. `self-host-catch-up` has its own line and log at minute 30 each hour, excluding hours 05 and 06. It updates the checkout, checks supervisor drift, releases the -checkout lock, then calls the seven installed job wrappers in order with `--due`. +checkout lock, then calls the eight installed job wrappers in order with `--due`. Each wrapper updates and rechecks due status under the shared lock, so a nominal run racing catch-up cannot repeat a completed period. It compares the latest nominal occurrence against the last successful start time. First install is due @@ -111,8 +119,8 @@ a busy lock exits 76 for retry next hour. A per-job overlap is also exit 76 and does not add a data-job run. Nominal jobs wait until ten seconds before their whole-run deadline. If they never acquire the checkout lock, they log a skipped run (76), not a failure. A timeout after acquiring the lock remains a failure. -Catch-up runs due jobs sequentially; its whole-run limit is 240 minutes, covering -the seven job limits (225 minutes total) and bounded lock waits. Its own per-job +Catch-up runs due jobs sequentially; its whole-run limit is 260 minutes, covering +the eight job limits (245 minutes total) and bounded lock waits. Its own per-job lock prevents overlapping catch-up sweeps. All cron starts avoid :35 to :50 and the 05:35 to 06:45 release window. @@ -124,7 +132,7 @@ never writes inferred editorial technical facts. ## Install after merge The lead installs cron only after the reviewed code reaches main. First ensure -`DATABASE_URL` and `GITHUB_TOKEN` exist and the Infisical sync succeeds. +`DATABASE_URL`, `GITHUB_TOKEN`, and `AWESOME_LIST_GITHUB_TOKEN` exist and the Infisical sync succeeds. `GITLAB_TOKEN` and `CODEBERG_TOKEN` are optional. If the clone does not exist, create it as ubuntu (sudo may be needed just for the directory): @@ -221,3 +229,74 @@ For pre-merge validation only, a manual `--validation /absolute/checkout` uses a working-tree copy, skips its Git update, and clearly labels the log. Cron never uses this flag. Delete that temporary validation checkout after testing. No env file belongs inside either checkout. + + +## Weekly public list: install and operate + +Built, waiting for install. After the reviewed merge, the lead adds +`AWESOME_LIST_GITHUB_TOKEN` to Infisical as described above and waits for the +sync to deliver the mode-600 env file. Do not edit that file on OVH. The existing +dataset sync uses `gh` with `GH_TOKEN`/`GITHUB_TOKEN`; this job uses Git HTTPS +with its own token and does not need an interactive login or `gh`. + +As ubuntu, update `/srv/stack127-self-host` with the fetch and fast-forward +commands above. **Reinstall the supervisor using the syntax-check and atomic +rename above**: this change modifies `run-job.sh`, including the job list and +catch-up time limit. The next run installs dependencies if needed. No separate +clone setup is needed. + +If the cron block is already installed, the append-once command above will do +nothing. Replace only the managed block, preserving all other entries: + +```sh +crontab -l > /srv/stack127-data/self-host-apps/crontab.before +sed '/^# BEGIN stack127 self-host apps/,/^# END stack127 self-host apps/d' /srv/stack127-data/self-host-apps/crontab.before > /srv/stack127-data/self-host-apps/crontab.new +cat /srv/stack127-self-host/scripts/self-host-apps/ovh/crontab.txt >> /srv/stack127-data/self-host-apps/crontab.new +cat /srv/stack127-data/self-host-apps/crontab.new +``` + +Review that file, then install and run the job (this publishes changed files): + +```sh +crontab /srv/stack127-data/self-host-apps/crontab.new +/bin/bash /srv/stack127-data/self-host-apps/run-job.sh self-host-awesome-list --due +tail -n 40 /srv/stack127-data/logs/self-host-awesome-list.log +crontab -l +``` + +Check the dashboard entry and the public repository commit. The job runs Monday +14:02 UTC (19:32 IST), after Monday's 12:26 technical refresh and its 60-minute +limit. It uses the existing per-job and checkout locks, output caps, scope and +success markers. Catch-up includes it last. A successful unchanged export is a +no-op; failed export, validation, commit or push leaves no success marker. + +The export explicitly uses `--database-env` with the validated production +connection, never `.env.local`. Each run creates `awesome-list-*` under +`/srv/stack127-data/self-host-apps/`, containing a fresh export and a fresh clone +of the public repository. It validates nonempty README, LICENSE, CSV, JSON, +README category links and matching category files, plus unique app slugs and +valid category memberships. Compared with the clone's current main, app count +may fall at most 10%; category count may not fall. Refusal prints the old/new +counts and fails before changing the clone. Only README, LICENSE, both data +files and category Markdown files are replaced/staged. No symlink is followed. +A bot commit includes both counts. Git calls time out after five minutes and +never force-push. A remote edit racing the push fails with Git's rejection; +the next run clones the new main and tries again. Git errors include the reason +with credentials redacted. + +Temporary work is removed on ordinary success/failure. A SIGKILL can leave a +scratch directory; it is never reused. After checking no list job is active, +the lead may remove only abandoned `awesome-list-*` scratch directories under +that exact state directory. They contain public exports/clones and an askpass +script with no token value. Do not remove state, reports, logs or lock files. + +For an intended large drop, review the source change and counts first, then use +this one-run override as ubuntu (it still checks all files and symlinks): + +```sh +AWESOME_LIST_ALLOW_COUNT_DROP=1 /bin/bash /srv/stack127-data/self-host-apps/run-job.sh self-host-awesome-list +``` + +Do not save the override in Infisical or cron. Its use logs a warning with the +old/new counts. Without `--due`, a manual run executes even if this week already +succeeded. The normal hourly retry keeps the count guard enabled. diff --git a/scripts/self-host-apps/ovh/crontab.txt b/scripts/self-host-apps/ovh/crontab.txt index 82db962ae..e15ce2a3c 100644 --- a/scripts/self-host-apps/ovh/crontab.txt +++ b/scripts/self-host-apps/ovh/crontab.txt @@ -10,5 +10,6 @@ 18 10 1 * * /bin/bash /srv/stack127-data/self-host-apps/run-job.sh self-host-discovery-directories --due >> /srv/stack127-data/logs/self-host-discovery-directories.log 2>&1 22 11 * * * /bin/bash /srv/stack127-data/self-host-apps/run-job.sh self-host-refresh-source-hosts --due >> /srv/stack127-data/logs/self-host-refresh-source-hosts.log 2>&1 26 12 * * 1 /bin/bash /srv/stack127-data/self-host-apps/run-job.sh self-host-refresh-technical --due >> /srv/stack127-data/logs/self-host-refresh-technical.log 2>&1 +2 14 * * 1 /bin/bash /srv/stack127-data/self-host-apps/run-job.sh self-host-awesome-list --due >> /srv/stack127-data/logs/self-host-awesome-list.log 2>&1 30 0-4,7-23 * * * /bin/bash /srv/stack127-data/self-host-apps/run-job.sh self-host-catch-up >> /srv/stack127-data/logs/self-host-catch-up.log 2>&1 # END stack127 self-host apps diff --git a/scripts/self-host-apps/ovh/due.mjs b/scripts/self-host-apps/ovh/due.mjs index a477682af..08c951abd 100644 --- a/scripts/self-host-apps/ovh/due.mjs +++ b/scripts/self-host-apps/ovh/due.mjs @@ -10,6 +10,7 @@ export function scheduledAt(job, now = new Date()) { 'self-host-discovery-directories': [10, 18, 'month'], 'self-host-refresh-source-hosts': [11, 22], 'self-host-refresh-technical': [12, 26, 1], + 'self-host-awesome-list': [14, 2, 1], }; const schedule = schedules[job]; if (!schedule) throw new Error(`Unknown job: ${job}`); diff --git a/scripts/self-host-apps/ovh/run-job.sh b/scripts/self-host-apps/ovh/run-job.sh index 56c9e1944..1690eb880 100755 --- a/scripts/self-host-apps/ovh/run-job.sh +++ b/scripts/self-host-apps/ovh/run-job.sh @@ -72,6 +72,8 @@ steps() { self-host-refresh-source-hosts) npm run self-host-apps:refresh-gitlab -- --published-only --limit 500 --max-age-hours 24 || rc=$? npm run self-host-apps:refresh-codeberg -- --published-only --limit 500 --max-age-hours 24 || rc=$? ;; + self-host-awesome-list) + npm run stack127:repo:awesome-list:sync ;; self-host-refresh-technical) npm run self-host-apps:refresh-primary-technical-facts -- --refresh-all --concurrency 16 --max-failure-percent 5 --out "$report_dir/$run_id-technical-refresh.json" || rc=$? # Deliberately dry-run (default); never add --apply here. @@ -199,7 +201,7 @@ catch_up() { # Child wrappers take their own shared lock. Do not inherit ours and deadlock. exec 8>&- local next next_rc rc=0 - for next in self-host-discovery-github self-host-refresh-github self-host-discovery-lists self-host-discovery-catalogs self-host-discovery-directories self-host-refresh-source-hosts self-host-refresh-technical; do + for next in self-host-discovery-github self-host-refresh-github self-host-discovery-lists self-host-discovery-catalogs self-host-discovery-directories self-host-refresh-source-hosts self-host-refresh-technical self-host-awesome-list; do local args=("$next" --due --catch-up) [[ "$validation" = 0 ]] || args+=(--validation "$checkout") next_rc=0 @@ -262,7 +264,7 @@ execute() { } # Use Node's dotenv parser; never evaluate secret values as shell code. export GITHUB_STEP_SUMMARY="$report_dir/$run_id-summary.md" - unset DATABASE_URL GITHUB_TOKEN GITLAB_TOKEN CODEBERG_TOKEN + unset DATABASE_URL GITHUB_TOKEN GITLAB_TOKEN CODEBERG_TOKEN AWESOME_LIST_GITHUB_TOKEN local started started=$(date +%s%3N) # Run the updated step definitions. Syntax/launcher failures reach the stable @@ -282,9 +284,10 @@ main() { self-host-discovery-catalogs) minutes=45 ;; self-host-discovery-directories|self-host-refresh-source-hosts) minutes=30 ;; self-host-refresh-technical) minutes=60 ;; - # Sum of the seven data-job limits is 225 minutes, plus bounded lock waits. - self-host-catch-up) minutes=240 ;; - *) echo 'Usage: run-job.sh self-host-{discovery-{github,lists,catalogs,directories},refresh-{github,source-hosts,technical},catch-up} [--due | --validation CHECKOUT]' >&2; exit 2 ;; + self-host-awesome-list) minutes=20 ;; + # Eight job limits total 245 minutes, plus bounded lock waits. + self-host-catch-up) minutes=260 ;; + *) echo 'Usage: run-job.sh self-host-{discovery-{github,lists,catalogs,directories},refresh-{github,source-hosts,technical},awesome-list,catch-up} [--due | --validation CHECKOUT]' >&2; exit 2 ;; esac log_file="$logs/$job.log" if [[ "$phase" = outer ]]; then diff --git a/scripts/self-host-apps/ovh/validate-env.mjs b/scripts/self-host-apps/ovh/validate-env.mjs index bdde68426..419ed1591 100644 --- a/scripts/self-host-apps/ovh/validate-env.mjs +++ b/scripts/self-host-apps/ovh/validate-env.mjs @@ -6,6 +6,7 @@ let database; try { database = new URL(process.env.DATABASE_URL); } catch { fail('DATABASE_URL is missing or invalid'); } if (database.hostname !== '127.0.0.1' || database.username !== 'stack127_app' || database.pathname !== '/postgres' || (database.port || '5432') !== '5432' || database.searchParams.get('sslmode') !== 'disable') fail('DATABASE_URL must use the loopback production app connection with sslmode=disable'); if (['self-host-discovery-github', 'self-host-discovery-catalogs', 'self-host-refresh-github', 'self-host-refresh-technical'].includes(job) && !process.env.GITHUB_TOKEN?.trim()) fail('GITHUB_TOKEN is missing'); +if (job === 'self-host-awesome-list' && !process.env.AWESOME_LIST_GITHUB_TOKEN?.trim()) fail('AWESOME_LIST_GITHUB_TOKEN is missing'); // GITLAB_TOKEN and CODEBERG_TOKEN are optional: both APIs answer public reads without them. // Descriptor 8 holds the checkout flock. The step shell must retain it if this launcher dies. const child = spawn('/bin/bash', [wrapper, '--steps', job, checkout, runId], { stdio: ['inherit', 'inherit', 'inherit', 'ignore', 'ignore', 'ignore', 'ignore', 'ignore', 8] }); diff --git a/scripts/stack127/app-categories/prepare-manifest.mjs b/scripts/stack127/app-categories/prepare-manifest.mjs index 8ac5d5afb..713dea999 100644 --- a/scripts/stack127/app-categories/prepare-manifest.mjs +++ b/scripts/stack127/app-categories/prepare-manifest.mjs @@ -17,7 +17,8 @@ try { if (category.count < MIN_CATEGORY_APPS || (args.category && category.slug !== args.category)) continue; const route = `app/(site)/(directories)/${directory}/categories/[category]`; const intro = `${route}/content/${category.slug}/intro.mdx`; - if (!existsSync(intro)) throw new Error(`Missing intro: ${intro}`); + const about = `${route}/content/${category.slug}/about.mdx`; + for (const [name, file] of [["intro", intro], ["about", about]]) if (!existsSync(file)) throw new Error(`Missing ${name}: ${file}`); const slug = `${directory}/categories/${category.slug}`; const page = (await client.query(`select content.title from public.stack127_directory_pages content join public.stack127_pages page on page.id=content.page_id where page.slug=$1 and page.page_family='directory'`, [slug])).rows[0]; @@ -25,7 +26,7 @@ try { pages.push({ slug: `${directory}/categories/${category.slug}`, family: 'directory', source: 'development', registry: { is_published: true, is_indexable: args.indexable === 'true' }, touch_reason: 'Category introduction and complete app list', - requirements: { route: `${route}/page.tsx`, assets: [intro], records: { kind: 'database', checks: [{ + requirements: { route: `${route}/page.tsx`, assets: [intro, about], records: { kind: 'database', checks: [{ name: 'Category has at least 15 published apps', sql_file: 'scripts/stack127/app-categories/records-ready.sql', params: [directory, category.slug], }] }, smoke: [categorySmokeCheck(directory, category.slug, page.title)] }, }); diff --git a/scripts/stack127/app-categories/sync-pages.mjs b/scripts/stack127/app-categories/sync-pages.mjs index ec2c07378..ebbcd0133 100644 --- a/scripts/stack127/app-categories/sync-pages.mjs +++ b/scripts/stack127/app-categories/sync-pages.mjs @@ -23,5 +23,5 @@ printResult(await runMutation(args, async client => { values($1,$2,$3,$4) on conflict(page_id) do nothing`, [page.id, `${category.name} ${directory === 'cli-apps' ? 'CLI apps' : 'self-host apps'}`, category.name, `Apps in the ${category.name} category.`]); rows.push({ slug, published: page.is_published, indexable: page.is_indexable, count: category.count }); } - return { directory, categories, rows, missing_intros: categories.filter(category => !existsSync(`app/(site)/(directories)/${directory}/categories/[category]/content/${category.slug}/intro.mdx`)).map(category => category.slug) }; + return { directory, categories, rows, missing_intros: categories.filter(category => ["intro", "about"].some(name => !existsSync(`app/(site)/(directories)/${directory}/categories/[category]/content/${category.slug}/${name}.mdx`))).map(category => category.slug) }; })); diff --git a/scripts/stack127/repos/export-awesome-list.mjs b/scripts/stack127/repos/export-awesome-list.mjs new file mode 100644 index 000000000..0db07ae09 --- /dev/null +++ b/scripts/stack127/repos/export-awesome-list.mjs @@ -0,0 +1,275 @@ +#!/usr/bin/env node + +// Build the files of the public awesome-self-hosted-apps repository from +// production data: a README with a category index, one Markdown table per +// category, and the same rows as CSV and JSON. Read-only; writes only --out. + +import path from "node:path"; +import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync, rmSync } from "node:fs"; +import { isIP } from "node:net"; +import { pathToFileURL } from "node:url"; +import { withClient } from "../../self-host-apps/lib/db.mjs"; +import { parseArgs, readEnvValues, requireArg, toCsv, writeText } from "./lib.mjs"; +import { renderDatasetLicense } from "./render-readme.mjs"; + +const site = "https://stack127.com"; +const labelSuffix = " self-hosted apps"; +const statusText = { active: "Active", slow: "Slow", stale: "Stale", archived: "Archived" }; +const dockerText = { official: "Official", community: "Community", none: "No" }; +const csvColumns = [ + "slug", "name", "summary", "categories", "website_url", "source_code_url", "other_url", "license", "source_model", + "maintenance_status", "last_commit_date", "docker_support", "stars", "ram_estimate_min_mb", "ram_estimate_max_mb", +]; + +const appsSql = ` + select app.slug, app.name, app.summary, app.website_url, app.source_code_url, app.docs_url, app.publisher_url, app.source_model, + coalesce(nullif(project.license_spdx_id, 'NOASSERTION'), app.license) as license, + app.maintenance_status, app.docker_support, project.stars, + coalesce(project.last_commit_at, app.source_last_commit_at, app.github_last_commit_at) as last_commit_at, + estimate.ram_estimate_min_mb, estimate.ram_estimate_max_mb, + coalesce(array_agg(category.slug order by category.sort_order, category.slug) filter (where category.slug is not null), '{}') as categories + from public.self_host_apps app + left join public.github_projects project on project.id = app.github_project_id + left join public.self_host_app_resource_estimates estimate on estimate.app_id = app.id + left join public.self_host_app_category_links link on link.app_id = app.id + left join public.self_host_app_categories category on category.id = link.category_id + where app.published and app.self_hosting_status = 'self_hostable' + group by app.id, project.id, estimate.app_id + order by lower(app.name), app.slug`; + +// Only categories with a published, indexable page are listed, so every heading link works. +const categoriesSql = ` + select category.slug, page.label + from public.self_host_app_categories category + join public.stack127_pages page on page.slug = 'self-host-apps/categories/' || category.slug + where page.is_published and page.is_indexable + order by category.sort_order, category.slug`; + +export const cell = (value) => String(value ?? "").replace(/\s+/g, " ").replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/([<>[\]`*_])/g, "\\$1").trim(); +const count = (value) => Number(value).toLocaleString("en-US"); +const title = "# Awesome self-hosted apps"; + +// A public link only: http or https, no credentials, no private or local host. +const localSuffix = /(^|\.)(localhost|local|internal|lan|home|ts\.net)$/; +export function httpUrl(value) { + if (typeof value !== "string" || /[\s\\]/.test(value.trim())) return ""; + let url; + try { url = new URL(value.trim()); } catch { return ""; } + if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) return ""; + // Bare IP addresses are never a project's public home, so all of them are refused. + const host = url.hostname.replace(/\.$/, ""); + if (!host.includes(".") || isIP(host.replace(/^\[|\]$/g, "")) || localSuffix.test(host)) return ""; + return url.href; +} + +// Percent-encode the characters that could end a Markdown link or a table cell, +// and escape "&" so Markdown cannot read an entity such as "@" in the URL. +const linkTarget = (url) => url.replace(/&/g, "&").replace(/[|()<>[\]`]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`); + +// Long names for the same license, as they appear in reviewed records. +const licenseIds = new Map([ + ["mit license", "MIT"], + ["apache license 2.0", "Apache-2.0"], + ["gnu affero general public license v3.0", "AGPL-3.0"], + ["gnu affero general public license v3.0 only", "AGPL-3.0"], + ["gnu affero general public license v3.0 or later", "AGPL-3.0-or-later"], + ["gnu general public license v3.0", "GPL-3.0"], + ["gnu general public license v3.0 or later", "GPL-3.0-or-later"], + ["gnu general public license v2.0", "GPL-2.0"], + ["gnu general public license v2.0 or later", "GPL-2.0-or-later"], + ["mozilla public license 2.0", "MPL-2.0"], + ["bsd 3-clause license", "BSD-3-Clause"], + ["business source license 1.1", "BUSL-1.1"], + ["elastic license 2.0", "Elastic-2.0"], + ["server side public license v1", "SSPL-1.0"], +]); + +export function licenseText(value) { + if (!value) return ""; + if (/^(no (software |open-source )?license|license not publicly)/i.test(value)) return "None declared"; + return licenseIds.get(value.trim().toLowerCase()) ?? value.trim(); +} + +const short = (value, max) => (value.length > max ? `${value.slice(0, max - 1).trimEnd()}…` : value); + +export function normalizeApp(row) { + const website = httpUrl(row.website_url); + const source = httpUrl(row.source_code_url); + return { + slug: row.slug, + name: row.name, + summary: row.summary ?? "", + categories: row.categories, + website_url: website, + source_code_url: source, + // Some appliance plugins have neither; their docs or publisher page is the only public link. + other_url: website || source ? "" : httpUrl(row.docs_url) || httpUrl(row.publisher_url), + license: licenseText(row.license), + source_model: row.source_model === "unknown" ? "" : row.source_model ?? "", + maintenance_status: row.maintenance_status === "unknown" ? "" : row.maintenance_status ?? "", + last_commit_date: row.last_commit_at ? new Date(row.last_commit_at).toISOString().slice(0, 10) : "", + docker_support: row.docker_support === "unknown" ? "" : row.docker_support ?? "", + stars: row.stars === null || row.stars === undefined ? null : Number(row.stars), + ram_estimate_min_mb: row.ram_estimate_min_mb === null ? null : Number(row.ram_estimate_min_mb), + ram_estimate_max_mb: row.ram_estimate_max_mb === null ? null : Number(row.ram_estimate_max_mb), + }; +} + +// The directory can hold one project twice, for example an appliance repackage +// such as "binhex-jellyfin" beside "Jellyfin". The list shows each source +// repository once and keeps the plainest entry. +// Host and path identify a repository; scheme, query, fragment, a trailing +// slash, and ".git" do not. Paths are case-insensitive only on these hosts. +const caseInsensitiveHosts = new Set(["github.com", "gitlab.com", "codeberg.org"]); +export function repositoryKey(value) { + const url = new URL(value); + const host = url.host.replace(/^www\./, ""); + const repoPath = url.pathname.replace(/\/+$/, "").replace(/\.git$/, ""); + return `${host}${caseInsensitiveHosts.has(url.hostname.replace(/^www\./, "")) ? repoPath.toLowerCase() : repoPath}`; +} +const repackaged = (name) => /^binhex-|-for-/i.test(name); + +export function onePerRepository(apps) { + const best = new Map(); + const better = (a, b) => Number(repackaged(a.name)) - Number(repackaged(b.name)) + || Number(!a.website_url) - Number(!b.website_url) + || a.name.length - b.name.length + || a.slug.localeCompare(b.slug); + for (const app of apps) { + if (!app.source_code_url) continue; + const key = repositoryKey(app.source_code_url); + if (!best.has(key) || better(app, best.get(key)) < 0) best.set(key, app); + } + return apps.filter((app) => !app.source_code_url || best.get(repositoryKey(app.source_code_url)) === app); +} + +function appRow(app) { + const home = app.website_url || app.source_code_url || app.other_url; + const name = home ? `[${cell(app.name)}](${linkTarget(home)})` : cell(app.name); + const source = app.source_code_url && app.source_code_url !== home ? ` ([source](${linkTarget(app.source_code_url)}))` : ""; + return `| ${name}${source} | ${cell(app.summary)} | ${cell(short(app.license, 40))} | ${statusText[app.maintenance_status] ?? ""} | ${dockerText[app.docker_support] ?? ""} | ${app.stars === null ? "" : count(app.stars)} |`; +} + +export function renderCategory(category, apps, date) { + const sorted = [...apps].sort((a, b) => (b.stars ?? -1) - (a.stars ?? -1) || a.name.localeCompare(b.name)); + return `# ${cell(category.name)} self-hosted apps + +${count(apps.length)} apps you can run on your own server, sorted by GitHub stars. Search and filter them on [stack127](${site}/self-host-apps/categories/${category.slug}). + +| App | What it does | License | Status | Docker | Stars | +| --- | --- | --- | --- | --- | ---: | +${sorted.map(appRow).join("\n")} + +[Back to all categories](../README.md). Data from ${date}. +`; +} + +export function renderReadme(categories, appsByCategory, total, date) { + const rows = categories.map((category) => `| [${cell(category.name)}](categories/${category.slug}.md) | ${count(appsByCategory.get(category.slug).length)} | [stack127.com](${site}/self-host-apps/categories/${category.slug}) |`); + return `${title} + +A list of ${count(total)} apps you can run on your own server, in ${categories.length} categories. Each app shows its license, whether it is still maintained, Docker support, and GitHub stars. + +The list is generated from the [stack127 self-hosted apps directory](${site}/self-host-apps), where you can search, filter, and sort the same apps. Data from ${date}. + +## Categories + +| Category | Apps | Search and filter | +| --- | ---: | --- | +${rows.join("\n")} + +An app can be in up to three categories. + +## Columns + +- **License**: the license the project declares. "None declared" means the repository has no license file, so all rights are reserved by default. +- **Status**: "Active", "Slow", "Stale", or "Archived", from the project's recent commits and releases. +- **Docker**: "Official" when the project publishes its own image, "Community" when someone else does, "No" when we found none. +- **Stars**: GitHub stars. Empty for apps hosted elsewhere or without public source. + +An empty cell means we have not verified that fact. + +## Data files + +- [data/apps.csv](data/apps.csv) and [data/apps.json](data/apps.json) hold every app once, with its categories, links, last commit date, and estimated RAM range. + +## Corrections + +The files here are generated, so pull requests that edit them are overwritten. To fix a fact or suggest an app, [open an issue](../../issues) with the app name and a link to the official source. + +## License + +The list and data are available under [CC BY 4.0](LICENSE). Credit stack127 with a link to ${site}. +`; +} + +// Refuse a folder that is not an earlier export, then remove only old category +// tables so a retired category leaves no stale file. +export function prepareOutDir(outDir) { + // A symlink here would send deletes and writes into some other folder. + for (const target of [outDir, path.join(outDir, "categories"), path.join(outDir, "data")]) { + if (lstatSync(target, { throwIfNoEntry: false })?.isSymbolicLink()) throw new Error(`${target} is a symbolic link`); + } + if (!existsSync(outDir) || readdirSync(outDir).length === 0) return; + const readme = path.join(outDir, "README.md"); + if (!existsSync(readme) || !readFileSync(readme, "utf8").startsWith(title)) { + throw new Error(`${outDir} is not empty and is not an earlier export of this list`); + } + const categoriesDir = path.join(outDir, "categories"); + if (!existsSync(categoriesDir)) return; + for (const entry of readdirSync(categoriesDir, { withFileTypes: true })) { + if (entry.isFile() && entry.name.endsWith(".md")) rmSync(path.join(categoriesDir, entry.name)); + } +} + +async function main() { + const args = parseArgs(); + const outDir = path.resolve(String(requireArg(args, "out"))); + const databaseUrl = args["database-env"] ? process.env.DATABASE_URL : readEnvValues(".env.database").DATABASE_URL; + if (!databaseUrl) throw new Error("Production DATABASE_URL is required in .env.database (or the environment with --database-env). Local .env.local is intentionally not used."); + + const { apps, categoryRows } = await withClient(async (client) => { + await client.query("begin transaction read only"); + try { + return { + apps: (await client.query(appsSql)).rows.map(normalizeApp), + categoryRows: (await client.query(categoriesSql)).rows, + }; + } finally { + await client.query("rollback"); + } + }, databaseUrl); + + const categories = categoryRows.map((row) => { + if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(row.slug)) throw new Error(`Unexpected category slug: ${row.slug}`); + if (!row.label?.endsWith(labelSuffix)) throw new Error(`Unexpected page label for ${row.slug}: ${row.label}`); + return { slug: row.slug, name: row.label.slice(0, -labelSuffix.length) }; + }); + if (!categories.length || !apps.length) throw new Error("No published categories or apps to export"); + + // Drop unpublished categories first, so the entry kept for a repository is one that can be listed. + const appsByCategory = new Map(categories.map((category) => [category.slug, []])); + for (const app of apps) app.categories = app.categories.filter((slug) => appsByCategory.has(slug)); + const categorized = apps.filter((app) => app.categories.length); + const listed = onePerRepository(categorized); + for (const app of listed) for (const slug of app.categories) appsByCategory.get(slug).push(app); + const date = new Date().toISOString().slice(0, 10); + + prepareOutDir(outDir); + writeText(path.join(outDir, "README.md"), renderReadme(categories, appsByCategory, listed.length, date)); + writeText(path.join(outDir, "LICENSE"), renderDatasetLicense()); + for (const category of categories) { + writeText(path.join(outDir, "categories", `${category.slug}.md`), renderCategory(category, appsByCategory.get(category.slug), date)); + } + writeText(path.join(outDir, "data", "apps.csv"), toCsv(listed.map((app) => ({ ...app, categories: app.categories.join("|") })), csvColumns)); + writeText(path.join(outDir, "data", "apps.json"), JSON.stringify({ generated: date, source: `${site}/self-host-apps`, apps: listed }, null, 1)); + + console.log(JSON.stringify({ out: outDir, apps: listed.length, repeatedRepositoryEntries: categorized.length - listed.length, withoutCategory: apps.length - categorized.length, categories: categories.length })); +} + +if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) { + main().catch((error) => { + console.error(error.stack ?? error.message); + process.exit(1); + }); +} diff --git a/scripts/stack127/repos/sync-awesome-list.mjs b/scripts/stack127/repos/sync-awesome-list.mjs new file mode 100644 index 000000000..afb7926d5 --- /dev/null +++ b/scripts/stack127/repos/sync-awesome-list.mjs @@ -0,0 +1,120 @@ +#!/usr/bin/env node +// Fresh clones make retries safe after interruption, including a failed push. +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { lstatSync, readdirSync, readFileSync, writeFileSync, mkdirSync, mkdtempSync, copyFileSync, rmSync } from 'node:fs'; +import { spawnSync } from 'node:child_process'; + +const remote = 'https://github.com/stack127labs/awesome-self-hosted-apps.git'; +const fixed = ['README.md', 'LICENSE', 'data/apps.csv', 'data/apps.json']; +const categoryName = /^[a-z0-9]+(?:-[a-z0-9]+)*\.md$/; + +// Check every ancestor, including the leaf. Never traverse a symlink. +export function safePath(target) { + const absolute = path.resolve(target); + let current = path.parse(absolute).root; + for (const part of absolute.slice(current.length).split(path.sep)) { + current = path.join(current, part); + if (lstatSync(current, { throwIfNoEntry: false })?.isSymbolicLink()) throw new Error(`Refusing symbolic link: ${current}`); + } + return absolute; +} +function regular(root, name) { + const target = safePath(path.join(root, name)); + const stat = lstatSync(target, { throwIfNoEntry: false }); + if (!stat?.isFile() || !stat.size) throw new Error(`Missing or empty generated file: ${name}`); + return target; +} +export function inspectExport(root) { + safePath(root); + for (const name of fixed) regular(root, name); + const readme = readFileSync(path.join(root, 'README.md'), 'utf8'); + if (!readme.startsWith('# Awesome self-hosted apps\n')) throw new Error('README is not an awesome-list export'); + const categories = [...readme.matchAll(/\]\(categories\/([a-z0-9]+(?:-[a-z0-9]+)*)\.md\)/g)].map(match => `${match[1]}.md`); + if (!categories.length || new Set(categories).size !== categories.length) throw new Error('README has missing or duplicate categories'); + const categoryDir = safePath(path.join(root, 'categories')); + for (const name of categories) regular(root, `categories/${name}`); + const files = readdirSync(categoryDir).filter(name => categoryName.test(name)); + if (files.length !== categories.length) throw new Error('Category files do not match README'); + const data = JSON.parse(readFileSync(path.join(root, 'data/apps.json'), 'utf8')); + if (!Array.isArray(data.apps) || !data.apps.length) throw new Error('JSON has no apps'); + const slugs = new Set(); + const known = new Set(categories.map(name => name.slice(0, -3))); + for (const app of data.apps) { + if (!app.slug || slugs.has(app.slug) || !Array.isArray(app.categories) || !app.categories.length || app.categories.some(slug => !known.has(slug))) throw new Error('JSON has duplicate apps or invalid categories'); + slugs.add(app.slug); + } + return { apps: data.apps.length, categories: categories.length, files: [...fixed, ...categories.map(name => `categories/${name}`)] }; +} +export function guardExport(next, previous, allowCountDrop = false) { + if (next.apps < previous.apps * 0.9 || next.categories < previous.categories) { + const detail = `apps ${previous.apps} -> ${next.apps}; categories ${previous.categories} -> ${next.categories}`; + if (!allowCountDrop) throw new Error(`Export refused: count guard (${detail}); maximum app drop 10%, no category drop`); + console.log(`WARNING: intentional count-drop override (${detail})`); + } +} +export function syncGenerated(source, destination, next = inspectExport(source)) { + inspectExport(destination); // Refuse an unrelated repository before deleting anything. + const old = readdirSync(safePath(path.join(destination, 'categories'))).filter(name => categoryName.test(name)); + // Preflight all writes/deletes before changing any file. + for (const name of [...next.files, ...old.map(name => `categories/${name}`)]) { + const stat = lstatSync(safePath(path.join(destination, name)), { throwIfNoEntry: false }); + if (stat && !stat.isFile()) throw new Error(`Refusing non-file generated path: ${name}`); + } + for (const name of next.files) { + regular(source, name); + mkdirSync(path.dirname(path.join(destination, name)), { recursive: true }); + copyFileSync(path.join(source, name), path.join(destination, name)); + } + for (const name of old) if (!next.files.includes(`categories/${name}`)) rmSync(path.join(destination, 'categories', name)); +} +function redact(text) { + for (const value of [process.env.AWESOME_LIST_GITHUB_TOKEN, process.env.DATABASE_URL]) if (value) text = text.replaceAll(value, '[redacted]'); + return text; +} +export function publishExport(source, destination, { git, allowCountDrop = false } = {}) { + const next = inspectExport(source); + guardExport(next, inspectExport(destination), allowCountDrop); + syncGenerated(source, destination, next); + // Stage only the generated paths, including removed category files. + git(['add', '--', ...fixed, 'categories/*.md']); + if (!git(['diff', '--cached', '--name-only']).trim()) { + console.log(`No changes: ${next.apps} apps, ${next.categories} categories`); + return false; + } + git(['-c', 'user.name=stack127 list bot', '-c', 'user.email=awesome-list-bot@users.noreply.github.com', '-c', 'commit.gpgsign=false', 'commit', '-m', `Refresh list: ${next.apps} apps, ${next.categories} categories`]); + git(['push', 'origin', 'HEAD:refs/heads/main']); + console.log(`Published ${next.apps} apps, ${next.categories} categories`); + return true; +} +export function runWeekly({ workRoot = '/srv/stack127-data/self-host-apps', allowCountDrop = process.env.AWESOME_LIST_ALLOW_COUNT_DROP === '1' } = {}) { + if (!process.env.AWESOME_LIST_GITHUB_TOKEN?.trim()) throw new Error('AWESOME_LIST_GITHUB_TOKEN is missing'); + safePath(workRoot); + mkdirSync(workRoot, { recursive: true }); + const work = mkdtempSync(path.join(workRoot, 'awesome-list-')); + const source = path.join(work, 'export'); + const destination = path.join(work, 'repo'); + const askpass = path.join(work, 'askpass.sh'); + // Only the variable name goes on disk. The token is never in args/config/remote. + writeFileSync(askpass, '#!/bin/sh\ncase "$1" in *Username*) printf "%s\\n" x-access-token ;; *) printf "%s\\n" "$AWESOME_LIST_GITHUB_TOKEN" ;; esac\n', { mode: 0o700 }); + const env = { ...process.env, GIT_AUTHOR_NAME: 'stack127 list bot', GIT_AUTHOR_EMAIL: 'awesome-list-bot@users.noreply.github.com', GIT_COMMITTER_NAME: 'stack127 list bot', GIT_COMMITTER_EMAIL: 'awesome-list-bot@users.noreply.github.com', GIT_TERMINAL_PROMPT: '0', GIT_ASKPASS: askpass, GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: '/dev/null' }; + const run = (command, args, cwd) => { + const result = spawnSync(command, args, { cwd, env, encoding: 'utf8', timeout: 300000, maxBuffer: 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] }); + if (result.error || result.status !== 0) throw new Error(redact(`${command} ${args[0]} failed (exit ${result.status ?? result.error?.code}): ${(result.stderr || '').slice(-8000)}`)); + return result.stdout; + }; + const git = args => run('git', ['-c', 'credential.helper=', '-c', 'http.followRedirects=false', '-c', 'http.lowSpeedLimit=1024', '-c', 'http.lowSpeedTime=30', ...args], destination); + try { + const exporter = fileURLToPath(new URL('./export-awesome-list.mjs', import.meta.url)); + run(process.execPath, [exporter, '--database-env', '--out', source]); + inspectExport(source); + // The clone is always fresh; no stale commit/index survives a failed run. + run('git', ['-c', 'credential.helper=', '-c', 'http.followRedirects=false', 'clone', '--depth', '1', '--branch', 'main', remote, destination]); + return publishExport(source, destination, { git, allowCountDrop }); + } finally { + rmSync(work, { recursive: true, force: true }); + } +} +if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) { + try { runWeekly(); } catch (error) { console.error(`ERROR: awesome-list failed: ${redact(error.message)}`); process.exitCode = 1; } +} diff --git a/stack127_docs/brand-growth/status.md b/stack127_docs/brand-growth/status.md index 9313f5aca..180a9c2dc 100644 --- a/stack127_docs/brand-growth/status.md +++ b/stack127_docs/brand-growth/status.md @@ -31,12 +31,21 @@ The 30 indexed pages are the home page, `/about`, `/privacy`, `/terms`, `/self-h | --- | --- | --- | | Request indexing for the 10 hub URLs, then the 10 largest self-host category pages the day after | Owner | Open | | Submit the remaining 148 sitemap URLs to Bing, 100 a day | Agent | Open | -| Build `stack127labs/awesome-self-hosted-apps` | Agent | In progress | +| Make `stack127labs/awesome-self-hosted-apps` public | Owner approves, agent flips | The repository is private with the first export: 4,814 apps in 42 categories | +| Merge the export script, pull request #120 | Agent, after the owner's go | Reviewed twice; waiting on the owner | +| Refresh the list on a schedule | Agent | built, waiting for install | | Pull the numbers and write the first data story | Agent | After the list is public | | Post the story on r/selfhosted | Owner | After review | | Measure referring domains as a baseline | Agent | Open | | CLI apps list, then ad networks list | Agent | After the first list | +## Known data gaps + +- 95 source repositories are listed twice or more in the directory, 194 entries in all, for example "Jellyfin" and "binhex-jellyfin". The public list shows each repository once. The directory still shows all of them, so its count is 4,913 and the list's is 4,814. Cleaning the directory changes category counts and needs its own data task. +- The directory has 4,913 published self-hostable apps. The category review covered 4,978 published apps, which includes entries that are not self-hostable. Use 4,913 or the list's 4,814 in the story, not 4,978. + ## Waiting on the owner - The "Request indexing" clicks in Search Console. +- A look at the private list repository, then a yes to make it public. +- A yes to merge pull request #120. diff --git a/stack127_docs/caching.md b/stack127_docs/caching.md index fe3722259..feca9fb79 100644 --- a/stack127_docs/caching.md +++ b/stack127_docs/caching.md @@ -49,9 +49,9 @@ Filters, search, sort, and pagination are a client component: Without JavaScript, visitors and crawlers see the default static page. -Self-host and CLI category pages have their own exact registry paths at `/self-host-apps/categories/` and `/cli-apps/categories/`. They list every category app in the server HTML, in the hub's default popularity order. They have no query-dependent server view or pagination. Only categories with at least 15 published apps can publish. `generateStaticParams` uses exact published registry children and current counts, including public noindex previews, so the static export contains the same pages as hub links. +Self-host and CLI category pages have their own exact registry paths at `/self-host-apps/categories/` and `/cli-apps/categories/`. They use the hub's layout, cards, details, filters, sort, and pagination, locked to one category. The static HTML holds the first page of that category in the hub's default popularity order, with those apps' details. Filtered views and later pages load in the browser and stay on the category path, with `noindex` query URLs like the hub. Self-host category pages send `category=` with every `/api/self-host-apps/search` request; the response's license and install options then cover only that category. The edge cache already keys that route by its full query, so no rule change is needed. CLI category pages filter the cached `/api/cli-apps/index` in the browser. Only categories with at least 15 published apps can publish. `generateStaticParams` uses exact published registry children and current counts, including public noindex previews, so the static export contains the same pages as hub links. -Category data uses the existing self-host and CLI source tags. The directory record trigger touches affected category children, including old memberships, and their hub. A category that drops below 15 apps is unpublished; growth does not republish it. Parent and child responses share `self-host-catalog` or `cli-apps-catalog` edge tags. A category page event also refreshes its hub links and purges the family, because sibling links show counts and publication. MDX intros ship with the release. +Category data uses the existing self-host and CLI source tags. The directory record trigger touches affected category children, including old memberships, and their hub. A category that drops below 15 apps is unpublished; growth does not republish it. Parent and child responses share `self-host-catalog` or `cli-apps-catalog` edge tags. A category page event also refreshes its hub links and purges the family, because sibling links show counts and publication. MDX intros and about sections ship with the release. ## Cache layers diff --git a/stack127_docs/directory-pages.md b/stack127_docs/directory-pages.md index 4e9acace1..f71d487ce 100644 --- a/stack127_docs/directory-pages.md +++ b/stack127_docs/directory-pages.md @@ -128,4 +128,6 @@ For launch directories, `evidence.public_submission` holds checked free-plan req The public paths are `/self-host-apps/categories/` and `/cli-apps/categories/`. Each child uses its own registry and directory row. The publisher treats the hub as its parent; `categories` is only a path segment. Only categories with at least 15 published apps can publish. If a published category drops below that number, the record trigger unpublishes it. A later increase does not publish it again. -Starting intros live at `app/(site)/(directories)//categories/[category]/content//intro.mdx`. Replace them with reviewed copy. Every intro is imported by that route's `content/index.ts`; add an import there when adding a new category. Use the fields editor above for the reviewed title, search title, description, and card fields. The draft sync never changes those fields or publishes a row. See [page-publishing.md](page-publishing.md#app-category-publication) for preview and release commands. +A category page has the same layout as its hub: filter sidebar, app cards and details, search, sort, and pagination, all locked to that category. The Category filter is hidden, and every link stays on the category path. Below the cards come the category links, with the current one marked, and a short about section. + +Each category has two MDX files in `app/(site)/(directories)//categories/[category]/content//`: `intro.mdx` above the apps and `about.mdx` below them. Replace starting copy with reviewed copy. Both are imported by that route's `content/index.ts` (`categoryIntros` and `categoryAbouts`); add both imports when adding a new category. The page fails to render when either is missing. Use the fields editor above for the reviewed title, search title, description, and card fields. The draft sync never changes those fields or publishes a row. See [page-publishing.md](page-publishing.md#app-category-publication) for preview and release commands. diff --git a/stack127_docs/monitoring.json b/stack127_docs/monitoring.json index 7be1a04db..524d47cf2 100644 --- a/stack127_docs/monitoring.json +++ b/stack127_docs/monitoring.json @@ -18,6 +18,7 @@ {"key": "github/stack127labs/stack127/ad-network-discovery.yml", "group": "Ad networks", "name": "Preview publisher discovery", "about": "A manual dry run that previews new publisher domains without saving anything."}, {"key": "github/stack127labs/stack127/ad-network-verification.yml", "group": "Ad networks", "name": "Preview publisher checks", "about": "A manual dry run that previews ad network checks without saving anything."}, {"key": "homelab/stack127-ad-network-niche", "group": "Ad networks", "name": "Sort sites into niches", "about": "Every day at 06:00 UTC (11:30 IST), an AI model sorts up to 3,000 unsorted publisher sites into niches and saves accepted labels to production.", "impact": "New sites stay out of the niche pages."}, + {"key": "stack127-vps/self-host-awesome-list", "group": "Self-host apps", "name": "Refresh public self-host list", "about": "Monday at 14:02 UTC, exports production apps, checks files and count drops, and pushes changed generated files to the public awesome-self-hosted-apps repository; catch-up retries missed runs.", "impact": "The public list and its downloadable data become stale."}, {"key": "stack127-vps/self-host-catch-up", "group": "Self-host apps", "name": "Catch up missed self-host jobs", "about": "At minute 30 each hour except 05 and 06 UTC, updates the checkout, checks the installed supervisor, and retries self-host jobs missing their latest scheduled success. Overlapping launches leave the active run log intact; checkout-lock waits skip for retry.", "impact": "Missed self-host jobs wait for their next nominal schedule, and supervisor drift goes unnoticed."}, {"key": "stack127-vps/self-host-discovery-catalogs", "group": "Self-host apps", "name": "Find apps in install catalogs", "about": "Sunday at 09:14 UTC, discovers apps from install catalogs; a separate catch-up job retries missed runs.", "impact": "New apps stop entering the install-catalog review queue."}, {"key": "stack127-vps/self-host-discovery-directories", "group": "Self-host apps", "name": "Find apps in directories", "about": "On the 1st at 10:18 UTC, discovers apps from public directories; a separate catch-up job retries missed runs.", "impact": "New apps stop entering the directory review queue."}, diff --git a/stack127_docs/self-host-apps-data.md b/stack127_docs/self-host-apps-data.md index d2523f217..b55d71aeb 100644 --- a/stack127_docs/self-host-apps-data.md +++ b/stack127_docs/self-host-apps-data.md @@ -217,6 +217,14 @@ npm run self-host-apps:apply-artifact-cleanup -- --environment production --conf Review the production preview before running the apply command. +For a separately reviewed batch, pass `--file scripts/self-host-apps/batches/` on both commands. The file uses the same `canonical_apps` and `records` format as the default manifest. Extra `decisions` describe duplicate, distinct and uncertain groups; only `records` are retired. A canonical app must remain published and self-hostable. A repeated target, or a canonical app that is also a target, stops the batch. + +Retirement keeps the app row, its category links, installation destinations and estimates, but sets `published=false`. Public directory, category, search and dataset queries exclude that row. Discovery dedupe deliberately indexes **all** app rows, including unpublished ones, so the retained source/homepage identities continue to block rediscovery. Existing published candidates also keep their completed state when discovery updates their evidence. Do not delete retired rows or globally draft other apps. + +Cleanup merges direct source, website, docs, install, Docker and Compose URLs, plus existing `data_source_urls`, into the kept app's evidence links. It does not copy structured facts across deployment variants. The November 2026 reviewed batch lists missing structured facts and resource estimates under `decisions[].facts_to_preserve`; review them before a separate fact correction. Package RAM caps, community image names and alternate category assignments must not silently replace upstream facts. + +Database triggers queue the directory and affected category refreshes, including the old category membership. A published category falling below `MIN_CATEGORY_APPS` (15) is unpublished by the trigger. In Cloudflare hosting mode the cache worker dispatches `stack127-frontend-refresh.yml`; that workflow rebuilds static pages and refreshes the OVH APIs. The database apply alone does not prove the public change is live. In legacy mode the worker invalidates and refreshes paths and query caches. See [Cloudflare database edits](cloudflare.md#database-edits-in-cloudflare-mode). Dataset files in the external awesome-list repository change only on its next exporter/sync run; the exporter filters unpublished apps. + The migration `20260901060529_streamline_self_host_apps_data_model.sql` folds the old rows into these tables. It removes pipeline-only tables and compatibility views and keeps the candidate review queue. ## Add one app on request diff --git a/stack127_docs/stack127-repos.md b/stack127_docs/stack127-repos.md index 293d6c648..d4f16dd2f 100644 --- a/stack127_docs/stack127-repos.md +++ b/stack127_docs/stack127-repos.md @@ -14,6 +14,7 @@ Apps, tools, and datasets that stack127 owns are independent stack127 repos. Eac | BaseBuddy app | Public | `stack127labs/basebuddy` | `main` | Public repository | Standalone open source app; no private runtime or build dependency | Active; `main` at `2ff50ab454805e498756d1013a1dc2af22acb7fd`; latest tag `v0.2.0` at `dd16eb96e4dac59b194f41e3e2d01472084d9172` checked 2026-08-19 | | DNS Tools | Public | `stack127labs/dns-tools` | `main` | Public repository | Standalone open source DNS tools; hosted stack127 pages get deliberate manual ports only | Active; `main` at `f437732151b4f35888c3b0e758dff79e90b99a24` checked 2026-08-19 | | Stack127 datasets | Public | `stack127labs/stack127-datasets` | `main` | Public repository | Generated public datasets; the source data stays in the private database and export workflow | Active; `main` at `807f61d709da84b14572214f9d214c392315b1f1` checked 2026-09-01 | +| Awesome self-hosted apps | Public | `stack127labs/awesome-self-hosted-apps` | `main` | Generated repository | Output of `export-awesome-list.mjs`; the source data stays in the private database | Created 2026-11-13; `main` at `7d70dc7` | The registry records how stack127 repos relate without copying their source trees here. Tool page runtime links and sync state also live in the `stack127_repo_*` columns on `public.stack127_tool_pages`. @@ -81,8 +82,10 @@ stack127 repo automation lives here: ```text scripts/stack127/repos/ create-tool-repo.mjs + export-awesome-list.mjs export-dataset.mjs render-readme.mjs + sync-awesome-list.mjs sync-dataset-repo.mjs templates/stack127-tool-repo/ @@ -93,12 +96,15 @@ Durable commands: ```bash npm run stack127:repo:dataset:export -- --surface self-host-apps --out /tmp/stack127-datasets npm run stack127:repo:dataset:sync -- --surface self-host-apps --repo stack127-datasets --dry-run +npm run stack127:repo:awesome-list:export -- --out /tmp/awesome-self-hosted-apps npm run stack127:repo:tool:new -- --slug port-checker --name "Port Checker" --dry-run ``` Dataset exports read verified production data, pick public columns, and sort rows the same way every time. They show a dry-run diff before any reviewed write or push. -Exports must use `.env.database` or an explicit production `--database-url`. They never fall back to `.env.local`. +The awesome-list export builds the files of `stack127labs/awesome-self-hosted-apps`: a README with a category index, one Markdown table per published category page, and `data/apps.csv` and `data/apps.json`. It lists each source repository once, so its app count is lower than the directory's. The OVH `self-host-awesome-list` job refreshes the public repository on Mondays at 14:02 UTC, with hourly catch-up. It exports into a fresh directory outside the code checkout, validates the files, refuses app-count drops over 10% and any category-count drop, then commits only changed generated files and pushes to `main`. Each run uses a fresh clone outside the code checkout, so interrupted commits and failed pushes cannot leave pending local history for the next run. Unrelated repository files stay intact. Setup, the repository-only token, and the manual count override are documented in [the OVH runner guide](../scripts/self-host-apps/ovh/README.md). Built, waiting for install. + +Exports must use `.env.database`, an explicit production `--database-url` where supported, or the awesome-list export’s `--database-env` option with the OVH runner’s validated production `DATABASE_URL`. They never fall back to `.env.local`. The tool scaffold is only a starting point. Once created, the new stack127 repo is its own source of truth and uses a separate checkout. diff --git a/tests/pages/cli-apps/navigation.test.mjs b/tests/pages/cli-apps/navigation.test.mjs index 099bdb1c0..448bdcb73 100644 --- a/tests/pages/cli-apps/navigation.test.mjs +++ b/tests/pages/cli-apps/navigation.test.mjs @@ -26,12 +26,9 @@ test("the site navigation leads to home, where Start here links to cli-apps", as }); test("the cli-apps page exposes language and license filters", async () => { - const page = await read("app/(site)/(directories)/cli-apps/page.tsx"); const view = await read("app/(site)/(directories)/cli-apps/cli-app-view.tsx"); assert.match(view, /name="language"/); assert.match(view, /name="license"/); - assert.match(page, /languageOptions/); - assert.match(page, /licenseOptions/); }); test("the directory filter logic supports license filtering", async () => { diff --git a/tests/self-host-apps/artifact-cleanup.test.mjs b/tests/self-host-apps/artifact-cleanup.test.mjs index 95269b5d5..4b62c1552 100644 --- a/tests/self-host-apps/artifact-cleanup.test.mjs +++ b/tests/self-host-apps/artifact-cleanup.test.mjs @@ -3,11 +3,18 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; -import { cleanupOptions } from "../../scripts/self-host-apps/apply-artifact-cleanup.mjs"; +import { cleanupOptions, targetSources, validateManifest } from "../../scripts/self-host-apps/apply-artifact-cleanup.mjs"; const developmentUrl = "postgresql://postgres.devref:secret@aws-0-eu.pooler.supabase.com:6543/postgres"; const productionUrl = "postgresql://stack127:secret@database.stack127.com:5432/stack127"; +test("retirement preserves reviewed evidence and Compose destinations alongside direct links", () => { + assert.deepEqual(targetSources({ source_code_url: "https://github.com/example/app", compose_url: "https://example.com/compose.yml", data_source_urls: ["https://example.com/install"], website_url: null }), [ + "https://github.com/example/app", "https://example.com/compose.yml", "https://example.com/install", + ]); + assert.deepEqual(targetSources({}), []); +}); + function withEnvFiles(callback) { const previous = process.cwd(); const dir = mkdtempSync(join(tmpdir(), "artifact-cleanup-")); @@ -42,3 +49,11 @@ test("production cleanup needs the shared confirm value, even for a preview", () assert.throws(() => cleanupOptions({ environment: "production", apply: true, confirm: "yes" }), /stack127-production/); }); }); + +test("a manifest with a misspelled or repeated app reference fails before any database work", () => { + const canonical_apps = [{ slug: "jellyfin", expected_names: ["Jellyfin"], source_code_url: "https://github.com/jellyfin/jellyfin" }]; + assert.doesNotThrow(() => validateManifest({ canonical_apps, records: [{ slug: "binhex-jellyfin", canonical_slug: "jellyfin" }, { slug: "artifact" }] })); + assert.throws(() => validateManifest({ canonical_apps, records: [{ slug: "binhex-jellyfin", canonical_slug: "jelyfin" }] }), /unknown canonical app jelyfin/); + assert.throws(() => validateManifest({ canonical_apps, records: [{ slug: "jellyfin", canonical_slug: "jellyfin" }] }), /also a retirement target/); + assert.throws(() => validateManifest({ canonical_apps: [...canonical_apps, ...canonical_apps], records: [] }), /listed twice/); +}); diff --git a/tests/self-host-apps/ovh-awesome-list.test.mjs b/tests/self-host-apps/ovh-awesome-list.test.mjs new file mode 100644 index 000000000..ac69f5508 --- /dev/null +++ b/tests/self-host-apps/ovh-awesome-list.test.mjs @@ -0,0 +1,119 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, symlinkSync, existsSync, realpathSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { execFileSync } from 'node:child_process'; +import { inspectExport, guardExport, syncGenerated, publishExport } from '../../scripts/stack127/repos/sync-awesome-list.mjs'; +import { scheduledAt } from '../../scripts/self-host-apps/ovh/due.mjs'; + +function fixture(t) { + const root = mkdtempSync(path.join(realpathSync(tmpdir()), 'awesome-sync-test-')); + t.after(() => rmSync(root, { recursive: true, force: true })); + return root; +} +function output(root, count = 10, categories = ['wikis']) { + mkdirSync(path.join(root, 'data'), { recursive: true }); + mkdirSync(path.join(root, 'categories'), { recursive: true }); + writeFileSync(path.join(root, 'README.md'), '# Awesome self-hosted apps\n' + categories.map(slug => `[${slug}](categories/${slug}.md)`).join('\n')); + writeFileSync(path.join(root, 'LICENSE'), 'CC BY 4.0\n'); + writeFileSync(path.join(root, 'data/apps.json'), JSON.stringify({ apps: Array.from({ length: count }, (_, i) => ({ slug: `app-${i}`, categories: [categories[0]] })) })); + writeFileSync(path.join(root, 'data/apps.csv'), 'slug,categories\napp-0,wikis\n'); + for (const slug of categories) writeFileSync(path.join(root, 'categories', `${slug}.md`), `# ${slug}\n`); + return root; +} +const git = (cwd, args) => execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); + +test('weekly list is due Monday 14:02 UTC and catches up across the week boundary', () => { + assert.equal(scheduledAt('self-host-awesome-list', new Date('2026-11-16T14:01:00Z')), Date.parse('2026-11-09T14:02:00Z')); + for (const now of ['2026-11-16T14:02:00Z', '2026-11-22T23:59:00Z']) assert.equal(scheduledAt('self-host-awesome-list', new Date(now)), Date.parse('2026-11-16T14:02:00Z')); +}); + +test('refuses missing files, missing category tables, empty apps and count drops; override affects counts only', t => { + const root = fixture(t); + const source = output(path.join(root, 'source')); + const original = inspectExport(source); + assert.doesNotThrow(() => guardExport({ apps: 9, categories: 1 }, original)); + assert.throws(() => guardExport({ apps: 8, categories: 1 }, original), /Export refused.*10%/); + assert.throws(() => guardExport({ apps: 10, categories: 0 }, original), /Export refused/); + assert.doesNotThrow(() => guardExport({ apps: 8, categories: 1 }, original, true)); + for (const name of ['README.md', 'LICENSE', 'data/apps.json', 'data/apps.csv', 'categories/wikis.md']) { + const target = path.join(source, name), bytes = readFileSync(target); + rmSync(target); + assert.throws(() => inspectExport(source)); + writeFileSync(target, bytes); + } + output(source, 0); + assert.throws(() => inspectExport(source), /no apps/); +}); + +test('sync removes retired category files and leaves unrelated files unchanged', t => { + const root = fixture(t); + const source = output(path.join(root, 'source'), 10, ['wikis', 'new']); + const destination = output(path.join(root, 'repo'), 10, ['wikis', 'retired']); + mkdirSync(path.join(destination, '.github')); + for (const name of ['CONTRIBUTING.md', '.github/settings.yml', 'data/notes.txt', 'categories/notes.txt']) writeFileSync(path.join(destination, name), 'keep me'); + syncGenerated(source, destination); + assert.equal(existsSync(path.join(destination, 'categories/retired.md')), false); + assert.equal(existsSync(path.join(destination, 'categories/new.md')), true); + for (const name of ['CONTRIBUTING.md', '.github/settings.yml', 'data/notes.txt', 'categories/notes.txt']) assert.equal(readFileSync(path.join(destination, name), 'utf8'), 'keep me'); +}); + +test('sync refuses source, destination file and ancestor symlinks without touching targets', t => { + const root = fixture(t); + const source = output(path.join(root, 'source')); + const destination = output(path.join(root, 'repo')); + const external = path.join(root, 'external'); + writeFileSync(external, 'keep me'); + rmSync(path.join(destination, 'LICENSE')); + symlinkSync(external, path.join(destination, 'LICENSE')); + assert.throws(() => syncGenerated(source, destination), /symbolic link/); + assert.equal(readFileSync(external, 'utf8'), 'keep me'); + symlinkSync(source, path.join(root, 'link')); + assert.throws(() => inspectExport(path.join(root, 'link')), /symbolic link/); +}); + +test('unchanged export is a no-op; changed export commits counts with bot identity and push failures propagate', t => { + const root = fixture(t); + const source = output(path.join(root, 'source')); + const repo = output(path.join(root, 'repo')); + git(repo, ['init', '-b', 'main']); + git(repo, ['add', '.']); + git(repo, ['-c', 'user.name=Fixture', '-c', 'user.email=fixture@example.invalid', 'commit', '-m', 'initial']); + const before = git(repo, ['rev-parse', 'HEAD']); + let pushes = 0; + const runGit = args => { + if (args[0] === 'push') { pushes++; throw new Error('push rejected: non-fast-forward'); } + return git(repo, args); + }; + assert.equal(publishExport(source, repo, { git: runGit }), false); + assert.equal(git(repo, ['rev-parse', 'HEAD']), before); + assert.equal(pushes, 0); + output(source, 11); + assert.throws(() => publishExport(source, repo, { git: runGit }), /push rejected: non-fast-forward/); + assert.equal(pushes, 1); + assert.match(git(repo, ['log', '-1', '--format=%s%n%an%n%ae%n%cn%n%ce']), /Refresh list: 11 apps, 1 categories\nstack127 list bot\nawesome-list-bot@users.noreply.github.com\nstack127 list bot/); +}); + +test('count refusal occurs before sync or git mutation', t => { + const root = fixture(t); + const source = output(path.join(root, 'source'), 8); + const repo = output(path.join(root, 'repo'), 10); + const before = readFileSync(path.join(repo, 'data/apps.json'), 'utf8'); + assert.throws(() => publishExport(source, repo, { git() { throw new Error('must not run git'); } }), /Export refused/); + assert.equal(readFileSync(path.join(repo, 'data/apps.json'), 'utf8'), before); +}); + +test('env preflight requires the dedicated token only for the list job', t => { + const root = fixture(t); + const wrapper = path.join(root, 'steps.sh'); + writeFileSync(wrapper, '#!/bin/bash\nexit 0\n'); + const validator = new URL('../../scripts/self-host-apps/ovh/validate-env.mjs', import.meta.url); + const env = { ...process.env, DATABASE_URL: 'postgresql://stack127_app@127.0.0.1:5432/postgres?sslmode=disable' }; + delete env.AWESOME_LIST_GITHUB_TOKEN; + const options = { env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe', 'ignore', 'ignore', 'ignore', 'ignore', 'ignore', 'ignore'] }; + assert.throws(() => execFileSync(process.execPath, [validator.pathname, 'self-host-awesome-list', wrapper, root, 'test'], options), error => /AWESOME_LIST_GITHUB_TOKEN is missing/.test(error.stderr)); + assert.doesNotThrow(() => execFileSync(process.execPath, [validator.pathname, 'self-host-discovery-lists', wrapper, root, 'test'], options)); + env.AWESOME_LIST_GITHUB_TOKEN = 'fixture-only'; + assert.doesNotThrow(() => execFileSync(process.execPath, [validator.pathname, 'self-host-awesome-list', wrapper, root, 'test'], options)); +}); diff --git a/tests/self-host-apps/ovh-checkout-recovery.test.mjs b/tests/self-host-apps/ovh-checkout-recovery.test.mjs index 8b1898f3d..2d486e3ad 100644 --- a/tests/self-host-apps/ovh-checkout-recovery.test.mjs +++ b/tests/self-host-apps/ovh-checkout-recovery.test.mjs @@ -181,7 +181,7 @@ test('installed supervisor logs checkout wrapper syntax failures, then fetches a .replaceAll('/srv/stack127-self-host', f.checkout) .replaceAll('/srv/stack127-data/logs', logs)); // Public dummy connection syntax passes preflight; no process connects to it. - writeFileSync(envFile, 'DATABASE_URL=postgresql://stack127_app@127.0.0.1:5432/postgres?sslmode=disable\nGITHUB_TOKEN=fixture-only\n', { mode: 0o600 }); + writeFileSync(envFile, 'DATABASE_URL=postgresql://stack127_app@127.0.0.1:5432/postgres?sslmode=disable\nGITHUB_TOKEN=fixture-only\nAWESOME_LIST_GITHUB_TOKEN=fixture-only\n', { mode: 0o600 }); mkdirSync(path.join(f.checkout, 'node_modules')); writeFileSync(path.join(f.root, 'dependencies.sha256'), `${createHash('sha256').update(lockfile).digest('hex')}\n`); const run = () => execFileSync('/bin/bash', [installed, job, '--due'], { encoding: 'utf8', timeout: 30_000, stdio: ['ignore', 'pipe', 'pipe'] }); @@ -219,7 +219,7 @@ test('installed supervisor logs checkout wrapper syntax failures, then fetches a const scopeOptions = { skip: process.platform !== 'linux' || !process.env.SELFHOST_TEST_SCOPE ? 'requires opt-in Linux systemd user scope' : false, }; -const jobs = ['discovery-github', 'refresh-github', 'discovery-lists', 'discovery-catalogs', 'discovery-directories', 'refresh-source-hosts', 'refresh-technical'].map(suffix => `self-host-${suffix}`); +const jobs = ['discovery-github', 'refresh-github', 'discovery-lists', 'discovery-catalogs', 'discovery-directories', 'refresh-source-hosts', 'refresh-technical', 'awesome-list'].map(suffix => `self-host-${suffix}`); function supervisorFixture(t, steps = 'echo "fixture-only-job $job"') { const f = fixture(t); @@ -240,7 +240,7 @@ function supervisorFixture(t, steps = 'echo "fixture-only-job $job"') { writeFileSync(path.join(f.origin, 'package-lock.json'), lockfile); git(f.origin, 'add', '.'); git(f.origin, 'commit', '-m', 'safe supervisor fixture'); - writeFileSync(`${f.root}.env`, 'DATABASE_URL=postgresql://stack127_app@127.0.0.1:5432/postgres?sslmode=disable\nGITHUB_TOKEN=fixture-only\n', { mode: 0o600 }); + writeFileSync(`${f.root}.env`, 'DATABASE_URL=postgresql://stack127_app@127.0.0.1:5432/postgres?sslmode=disable\nGITHUB_TOKEN=fixture-only\nAWESOME_LIST_GITHUB_TOKEN=fixture-only\n', { mode: 0o600 }); mkdirSync(path.join(f.checkout, 'node_modules')); writeFileSync(path.join(f.root, 'dependencies.sha256'), `${createHash('sha256').update(lockfile).digest('hex')}\n`); const run = (job = 'self-host-catch-up', args = []) => execFileSync('/bin/bash', [installed, job, ...args], { diff --git a/tests/stack127/app-categories.test.mjs b/tests/stack127/app-categories.test.mjs index 08b3a3d4c..c066eb4d0 100644 --- a/tests/stack127/app-categories.test.mjs +++ b/tests/stack127/app-categories.test.mjs @@ -1,6 +1,6 @@ import test from 'node:test'; import assert from 'node:assert/strict'; -import { countAppCategories, publishedAppCategories, appCategoryStaticParams, selectCategoryApps } from '../../lib/app-category-model.ts'; +import { countAppCategories, publishedAppCategories, appCategoryStaticParams } from '../../lib/app-category-model.ts'; import { targetsForEvents } from '../../scripts/stack127/cache/targets.mjs'; test('multi-category counts count each app once, preserve zeroes, and include all records', () => { @@ -19,14 +19,6 @@ test('only exact published directory children at the 15-app threshold get links assert.deepEqual(appCategoryStaticParams(links), [{ category: 'ready' }, { category: 'noindex' }]); }); -test('a complete category keeps the hub default popularity order, name ties, null scores, and multiple memberships', () => { - const apps = [{ name: 'Z', categories: ['other','wanted'], score: 8 }, { name: 'A', categories: ['wanted'], score: 8 }, - { name: 'B', categories: ['wanted'], score: null }, { name: 'C', categories: ['other'], score: 100 }]; - assert.deepEqual(selectCategoryApps(apps, 'wanted', app => app.categories, app => app.score ?? 0).map(app => app.name), ['A','Z','B']); - assert.equal(apps[0].name, 'Z'); - assert.equal(selectCategoryApps(Array.from({ length: 1201 }, (_, i) => ({ name: String(i), categories: ['wanted'], score: i })), 'wanted', app => app.categories, app => app.score).length, 1201); -}); - test('category publication changes refresh hub links, registry, sitemap, and family purge tags', () => { for (const directory of ['self-host-apps','cli-apps']) { const result = targetsForEvents([{ entity_type: 'page', slug: `${directory}/categories/example` }]); @@ -38,20 +30,6 @@ test('category publication changes refresh hub links, registry, sitemap, and fam } }); -test('compact server rows escape dataset text and reject executable outbound URLs', async () => { - const { categoryListHtml } = await import('../../lib/app-category-html.ts'); - const html = categoryListHtml([{ name: '', summary: '', facts: 'A&B', href: 'javascript:alert(1)' }, - { name: 'Safe', summary: 'Summary', facts: 'License', href: 'https://example.com/?a="&b=' }]); - assert.ok(!html.includes('