3 Commits
Author SHA1 Message Date
RGJorge 1273ff6f01 v0.1.2 — dynamic CI status badge in README
Swap the static "tests-37-passing" badge for the live GitHub Actions badge
now that CI is back online. README visitors see the real build status.
2026-05-12 02:46:11 +00:00
RGJorge 0038a7f20d v0.1.1 — fix UI overflow with long container/project names
Closes #1

- ServiceNode: cap card width at 240px, native tooltip on name and image
- App: project filter dropdown capped at 280px with scroll, names truncate
- DetailPanel header: proper flex layout (min-w-0 flex-1 + shrink-0),
  Tooltip with (?) shows full name on hover; same fix for logs modal header
- Re-enable CI workflow (was paused for GitHub billing)
2026-05-12 02:40:50 +00:00
RGJorge 396873ae87 v0.1.0 — Repositorio público 2026-05-11 03:02:14 +00:00
7 changed files with 29 additions and 24 deletions
+2
View File
@@ -14,5 +14,7 @@ data/
# Local notes / marketing — not part of the public repo
linkdin.md
recomendaciones.md
docs/reddit.md
# AI assistant context — internal, not for public repo
CLAUDE.md
+1 -1
View File
@@ -1,6 +1,6 @@
# ContainerFlow
[![CI](https://github.com/RGJorge/containerflow/actions/workflows/ci.yml/badge.svg)](https://github.com/RGJorge/containerflow/actions/workflows/ci.yml)
[![CI](https://github.com/RGJorge/ContainerFlow/actions/workflows/ci.yml/badge.svg)](https://github.com/RGJorge/ContainerFlow/actions/workflows/ci.yml)
[![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
[![Release](https://img.shields.io/github/v/tag/RGJorge/containerflow?label=version&color=green)](https://github.com/RGJorge/containerflow/tags)
![Docker Required](https://img.shields.io/badge/Docker-required-blue?logo=docker)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "containerflow",
"version": "0.1.0",
"version": "0.1.2",
"license": "AGPL-3.0-or-later",
"author": "Jorge Gonzalez D. (RGJorge)",
"type": "module",
+5 -4
View File
@@ -671,7 +671,7 @@ function Dashboard({ token }: { token: string }) {
<ChevronDown size={14} className={`text-slate-500 transition-transform ${filterOpen ? "rotate-180" : ""}`} />
</button>
{filterOpen && (
<div className="absolute top-full right-0 mt-1.5 bg-slate-800 border border-slate-700 rounded-lg shadow-xl shadow-black/40 py-1.5 min-w-[220px]">
<div className="absolute top-full right-0 mt-1.5 bg-slate-800 border border-slate-700 rounded-lg shadow-xl shadow-black/40 py-1.5 min-w-[220px] max-h-[280px] overflow-y-auto">
{/* Select/Deselect all */}
<button
onClick={() => {
@@ -706,15 +706,16 @@ function Dashboard({ token }: { token: string }) {
<button
key={p}
onClick={() => toggleProject(p)}
title={p}
className="flex items-center gap-2.5 w-full px-3.5 py-2 text-sm hover:bg-slate-700/60 transition-colors"
>
<div className={`w-4 h-4 rounded border flex items-center justify-center ${
<div className={`w-4 h-4 rounded border flex items-center justify-center shrink-0 ${
active ? "bg-cyan-500 border-cyan-500" : "border-slate-600"
}`}>
{active && <Check size={12} className="text-white" />}
</div>
<span className={active ? "text-slate-200" : "text-slate-500"}>{p}</span>
<span className="ml-auto flex items-center gap-1.5 text-xs">
<span className={`flex-1 min-w-0 truncate text-left ${active ? "text-slate-200" : "text-slate-500"}`}>{p}</span>
<span className="ml-auto flex items-center gap-1.5 text-xs shrink-0">
<span className="text-emerald-500/70">{running}</span>
<span className="text-slate-600">/</span>
<span className="text-slate-400">{projectServices.length}</span>
+3 -3
View File
@@ -141,7 +141,7 @@ export const ServiceNode = memo(function ServiceNode({ data, id }: NodeProps) {
return (
<div
className={`relative rounded-xl border ${s.border} ${s.bg} backdrop-blur-sm
shadow-lg shadow-black/30 p-4 min-w-[220px] ring-2 ${s.ring}
shadow-lg shadow-black/30 p-4 min-w-[220px] max-w-[240px] ring-2 ${s.ring}
transition-[opacity,box-shadow] duration-300 ${flashClass} ${d.locked ? "opacity-70" : ""}`}
>
{d.locked && (
@@ -181,7 +181,7 @@ export const ServiceNode = memo(function ServiceNode({ data, id }: NodeProps) {
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-bold text-white text-sm truncate">{d.label}</span>
<span className="font-bold text-white text-sm truncate" title={d.label}>{d.label}</span>
{d.state === "processing" ? (
<div className="flex items-center gap-0.5 shrink-0">
<div className="w-2 h-2 rounded-full bg-yellow-500 animate-pulse" />
@@ -193,7 +193,7 @@ export const ServiceNode = memo(function ServiceNode({ data, id }: NodeProps) {
<div className={`w-2 h-2 rounded-full shrink-0 ${s.dot}`} />
)}
</div>
<div className="text-xs text-slate-500 truncate mt-0.5">
<div className="text-xs text-slate-500 truncate mt-0.5" title={d.image}>
{d.image.startsWith("sha256:") ? `${t("node.noTag")} (${d.image.slice(7, 19)})` : d.image}
</div>
</div>
+17 -15
View File
@@ -394,12 +394,13 @@ export function DetailPanel({ service, stats, logLines, token, closing, locked,
className={`absolute top-0 left-0 bottom-0 w-[900px] bg-slate-900/95 backdrop-blur-sm border-r border-slate-700/60 flex flex-col z-50 rounded-l-xl transition-transform duration-[400ms] ease-out ${visible ? "translate-x-0" : "-translate-x-full"}`}
>
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-800 shrink-0">
<div className="flex items-center gap-2.5">
<span className={`w-2 h-2 rounded-full ${stateDot}`} />
<span className="text-sm font-semibold text-white truncate">{service.name}</span>
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-800 shrink-0 gap-3">
<div className="flex items-center gap-2.5 min-w-0 flex-1">
<span className={`w-2 h-2 rounded-full shrink-0 ${stateDot}`} />
<span className="text-sm font-semibold text-white truncate min-w-0 max-w-[240px]">{service.name}</span>
<Tooltip text={service.name} placement="bottom" width="w-72" size={12} />
{locked && (
<span className="flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium text-slate-400 bg-slate-700/60 border border-slate-600/50 rounded" title={t("access.viewOnly")}>
<span className="flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium text-slate-400 bg-slate-700/60 border border-slate-600/50 rounded shrink-0" title={t("access.viewOnly")}>
<Lock size={10} />
{t("access.viewOnly")}
</span>
@@ -409,20 +410,20 @@ export function DetailPanel({ service, stats, logLines, token, closing, locked,
href={`http://${window.location.hostname}:${service.ports[0].host}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-slate-500 hover:text-cyan-400 transition-colors"
className="flex items-center gap-1 text-slate-500 hover:text-cyan-400 transition-colors shrink-0"
title={`Open http://${window.location.hostname}:${service.ports[0].host}`}
>
<ExternalLink size={12} />
<span className="text-[11px] font-mono">:{service.ports[0].host}</span>
</a>
)}
<span className={`text-xs font-mono ${stateColor} flex items-center gap-1`}>
<span className={`text-xs font-mono ${stateColor} flex items-center gap-1 shrink-0`}>
{isProcessing ? `processing... ${elapsed}s` :
isCrashed ? <><AlertTriangle size={11} />crashed (exit {service.exit_code}{service.oom_killed ? ", OOM" : ""})</> :
service.state}
</span>
</div>
<div className="flex items-center gap-1.5">
<div className="flex items-center gap-1.5 shrink-0">
{/* Action buttons */}
{isProcessing ? (
<div className="flex items-center gap-1.5 px-2 py-1 text-[11px] font-medium text-yellow-400">
@@ -1265,16 +1266,17 @@ export function DetailPanel({ service, stats, logLines, token, closing, locked,
{/* Logs fullscreen modal */}
{logsModal && (
<div className="fixed inset-0 z-[100] bg-slate-900 flex flex-col">
<div className="flex items-center justify-between px-6 py-3 border-b border-slate-800 shrink-0">
<div className="flex items-center gap-2.5">
<Terminal size={16} className="text-cyan-400" />
<span className="text-sm font-semibold text-white">{service.name}</span>
<span className="text-xs text-slate-500 font-mono">logs</span>
<div className="flex items-center justify-between px-6 py-3 border-b border-slate-800 shrink-0 gap-3">
<div className="flex items-center gap-2.5 min-w-0 flex-1">
<Terminal size={16} className="text-cyan-400 shrink-0" />
<span className="text-sm font-semibold text-white truncate min-w-0 max-w-[240px]">{service.name}</span>
<Tooltip text={service.name} placement="bottom" width="w-72" size={12} />
<span className="text-xs text-slate-500 font-mono shrink-0">logs</span>
{service.state === "running" && subscribedRef.current && (
<span className="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-pulse" />
<span className="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-pulse shrink-0" />
)}
</div>
<div className="flex items-center gap-1">
<div className="flex items-center gap-1 shrink-0">
<button
onClick={() => {
const text = allLines.map((l) => `${l.timestamp ? formatTimestamp(l.timestamp) + " " : ""}${l.line}`).join("\n");