> ## Documentation Index
> Fetch the complete documentation index at: https://felimet-hub.jmcores.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 工程專案：自託管、機器學習與開發工具

> 工程專案作品集，內容涵蓋 Docker 自託管服務、Cloudflare Tunnel 對外部署、PyQt6 桌面應用、YOLO 與 SAM 電腦視覺工具，以及 IoT 溫濕度監測系統等實作案例。

export const projects = [{
  "title": "自建 Bitwarden 密碼管理伺服器",
  "description": "以 Docker Compose 搭配 Cloudflare Tunnel 部署自建 Bitwarden 密碼管理伺服器，零開放 Port、主機 IP 不暴露於外網，提供 Vaultwarden、官方 Lite 版與官方標準版三套配置選擇，並涵蓋 TLS、備份與資料遷移實務。",
  "href": "/projects/bitwarden-server",
  "tags": ["Bitwarden", "Vaultwarden", "Docker", "Cloudflare Tunnel", "Self-Hosted", "Security"],
  "featured": false,
  "order": 6,
  "date": "2026-03",
  "pubType": "",
  "venue": "",
  "confType": "",
  "award": ""
}, {
  "title": "Cloudflare R2 檔案上傳與管理系統",
  "description": "以 Cloudflare Workers 與 R2 物件儲存建構的無伺服器檔案上傳與管理系統，無檔案大小限制，特別適合儲存 .pt、.ckpt、.safetensors 等深度學習模型檔，提供瀏覽、刪除與分享功能，透過 wrangler CLI 一鍵部署。",
  "href": "/projects/cf-auto-deploy-worker",
  "tags": ["Cloudflare Workers", "Cloudflare R2", "JavaScript", "Serverless", "Wrangler"],
  "featured": false,
  "order": 9,
  "date": "2025-04",
  "pubType": "",
  "venue": "",
  "confType": "",
  "award": ""
}, {
  "title": "溫濕度監控系統",
  "description": "以 Tapo H200 網關搭配 T315 子設備感測器為資料來源，Python 採集器自動發現感測器並每 60 秒寫入 CrateDB 時序資料庫，Apache Superset 提供溫濕度儀表板與歷史趨勢分析，Docker Compose 封裝可部署至 Synology NAS 或本機。",
  "href": "/projects/climate-monitor",
  "tags": ["Python", "IoT", "CrateDB", "Docker", "Apache Superset"],
  "featured": false,
  "order": 4,
  "date": "2026-03",
  "pubType": "",
  "venue": "",
  "confType": "",
  "award": ""
}, {
  "title": "乳牛跛行偵測桌面 GUI",
  "description": "即時乳牛跛行偵測 PyQt6 桌面應用程式，採用 YOLO11m-seg 進行個體分割，並以 MobileNetV3-Large 分類器解析 GEI 與 HEI 步態能量影像輸出跛行等級結果，介面支援 RTSP 串流與本地影片檔案輸入，適用於畜牧場即時影像監測。",
  "href": "/projects/dairycow-lameness-gui",
  "tags": ["Python", "PyTorch", "YOLO", "Computer Vision", "Precision Livestock"],
  "featured": false,
  "order": 7,
  "date": "2025-10",
  "pubType": "",
  "venue": "",
  "confType": "",
  "award": ""
}, {
  "title": "GPUStack 推論服務部署",
  "description": "在單台 GPU 主機上以 Docker Compose 同時啟動 GPUStack 的 server 與 worker，透過 Cloudflare Tunnel 對外提供 OpenAI 相容的 LLM 推論 API，主機不開放任何入站埠號，並涵蓋 NVIDIA 驅動、模型載入與認證設定流程。",
  "href": "/projects/gpustack-api",
  "tags": ["GPUStack", "LLM", "Cloudflare Tunnel", "Docker", "NVIDIA"],
  "featured": false,
  "order": 1,
  "date": "2026-05",
  "pubType": "",
  "venue": "",
  "confType": "",
  "award": ""
}, {
  "title": "Label Anything SAM 標註平台",
  "description": "整合 Label Studio、Supabase、MinIO 物件儲存與 SAM3 / SAM2.1 GPU 推論後端的容器化影像標註堆疊，透過 Cloudflare Tunnel 對外曝露 Web 介面，主機不開放任何入站埠號，可協作完成大規模物件分割標註任務。",
  "href": "/projects/label-anything-sam",
  "tags": ["Label Studio", "SAM", "Docker", "Supabase", "MinIO", "Cloudflare Tunnel"],
  "featured": false,
  "order": 2,
  "date": "2026-04",
  "pubType": "",
  "venue": "",
  "confType": "",
  "award": ""
}, {
  "title": "SAM2 影像分割標註工具",
  "description": "基於 Meta SAM2 模型的 Windows 桌面影像分割標註應用程式，透過互動式點擊即可對影像目標進行快速分割遮罩生成，支援 CUDA GPU 加速推論，並提供 PyInstaller 封裝執行檔與從源碼建置兩種安裝方式，可整合至既有影像處理與標註工作流程。",
  "href": "/projects/sam2-annotation-tool",
  "tags": ["SAM2", "Python", "Computer Vision", "Annotation", "Windows", "CUDA"],
  "featured": false,
  "order": 8,
  "date": "2025-04",
  "pubType": "",
  "venue": "",
  "confType": "",
  "award": ""
}, {
  "title": "自託管 RustDesk 遠端桌面伺服器",
  "description": "以社群版 lejianwen/rustdesk-server-s6 映像檔在單一 Docker 容器內同時執行 hbbs 訊號伺服器、hbbr 中繼伺服器與 Web API 管理後台，透過 Cloudflare Tunnel 對外曝露 Web Console，無裝置數量限制，可完整自託管遠端桌面服務。",
  "href": "/projects/self-host-rustdesk-server",
  "tags": ["RustDesk", "Docker", "Cloudflare Tunnel", "Self-Hosted", "Remote Desktop"],
  "featured": false,
  "order": 5,
  "date": "2026-03",
  "pubType": "",
  "venue": "",
  "confType": "",
  "award": ""
}, {
  "title": "Synology NAS 萬用字元憑證自動更新",
  "description": "在 Synology NAS 上以 acme.sh 搭配 Cloudflare DNS-01 驗證申請與自動更新 Let's Encrypt 萬用字元 TLS 憑證，透過 synology_dsm deploy hook 自動部署至 DSM 管理介面，無需開放 80 與 443 連接埠。",
  "href": "/projects/synology-letsencrypt-cloudflare",
  "tags": ["Synology", "acme.sh", "Let's Encrypt", "Cloudflare", "TLS", "Shell"],
  "featured": false,
  "order": 3,
  "date": "2026-03",
  "pubType": "",
  "venue": "",
  "confType": "",
  "award": ""
}];

export const Gallery = ({items = [], type = "projects", lang = "zh"}) => {
  const TAG_LIMIT = 12;
  const t = lang === "en" ? {
    all: "All",
    more: n => `+${n} more`,
    less: "Show less",
    filter: "Filter",
    unit: n => `${n} items`,
    featured: "Featured first",
    date: "Newest first",
    title: "Title A→Z"
  } : {
    all: "全部",
    more: n => `+${n} 更多`,
    less: "收起",
    filter: "篩選",
    unit: n => `${n} 篇`,
    featured: "精選優先",
    date: "日期新→舊",
    title: "標題 A→Z"
  };
  const toTags = v => Array.isArray(v) ? v : typeof v === "string" && v.trim().startsWith("[") ? (() => {
    try {
      return JSON.parse(v);
    } catch {
      return [];
    }
  })() : v ? [String(v)] : [];
  const tagCount = {};
  for (const it of items) for (const tg of toTags(it.tags)) tagCount[tg] = (tagCount[tg] || 0) + 1;
  const sortedTags = Object.keys(tagCount).sort((a, b) => tagCount[b] - tagCount[a]);
  const [tag, setTag] = useState("all");
  const [sort, setSort] = useState(type === "publications" ? "date" : "featured");
  const [showAllTags, setShowAllTags] = useState(false);
  const visibleTags = showAllTags ? sortedTags : sortedTags.slice(0, TAG_LIMIT);
  const filtered = tag === "all" ? items : items.filter(i => toTags(i.tags).includes(tag));
  const shown = [...filtered].sort((a, b) => {
    if (sort === "title") return a.title.localeCompare(b.title, lang === "en" ? "en" : "zh-TW");
    if (sort === "date") return String(b.date).localeCompare(String(a.date));
    return (b.featured ? 1 : 0) - (a.featured ? 1 : 0) || a.order - b.order;
  });
  return <div>
      <div style={{
    marginBottom: "1.75rem"
  }}>
        <div style={{
    display: "flex",
    justifyContent: "space-between",
    alignItems: "center",
    marginBottom: "0.6rem"
  }}>
          <span className="g-filter-label">{t.filter} · {t.unit(shown.length)}</span>
          <select className="g-sort" value={sort} onChange={e => setSort(e.target.value)}>
            <option value="featured">{t.featured}</option>
            <option value="date">{t.date}</option>
            <option value="title">{t.title}</option>
          </select>
        </div>
        <div style={{
    display: "flex",
    flexWrap: "wrap",
    gap: "0.4rem",
    alignItems: "center"
  }}>
          <button className={"g-chip" + (tag === "all" ? " g-chip-on" : "")} onClick={() => setTag("all")}>{t.all}</button>
          {visibleTags.map(tg => <button key={tg} className={"g-chip" + (tag === tg ? " g-chip-on" : "")} onClick={() => setTag(tg)}>{tg}</button>)}
          {sortedTags.length > TAG_LIMIT && <button className="g-more" onClick={() => setShowAllTags(!showAllTags)}>
              {showAllTags ? t.less : t.more(sortedTags.length - TAG_LIMIT)}
            </button>}
        </div>
      </div>

      <div style={{
    display: "grid",
    gridTemplateColumns: "repeat(auto-fill, minmax(290px, 1fr))",
    gap: "1rem"
  }}>
        {shown.map(i => {
    const tags = toTags(i.tags);
    return <a key={i.href} href={i.href} className="g-card">
              <div className="g-card-title">
                {i.featured && <span style={{
      color: "#bf7551",
      marginRight: "0.3rem"
    }}>★</span>}
                {i.title}
              </div>
              <div className="g-card-desc">
                {i.description.length > 88 ? i.description.slice(0, 88) + "…" : i.description}
              </div>
              {(i.venue || tags.length > 0) && <div className="g-card-meta">
                  {i.venue && <div className="g-card-venue">{i.venue}</div>}
                  <div style={{
      display: "flex",
      flexWrap: "wrap",
      gap: "0.3rem"
    }}>
                    {tags.slice(0, 3).map(tg => <span key={tg} className="g-tag">{tg}</span>)}
                    {tags.length > 3 && <span className="g-tag-more">+{tags.length - 3}</span>}
                  </div>
                </div>}
            </a>;
  })}
      </div>
    </div>;
};

<Gallery items={projects} type="projects" />
