Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Run the full stack locally with Docker:
```bash
git clone https://github.com/ParleSec/ProtocolSoup.git
cd ProtocolSoup/docker
docker compose up -d
docker compose up -d --build
```

Then open:
Expand Down
18 changes: 18 additions & 0 deletions backend/cmd/gateway/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,28 @@ import (

"github.com/ParleSec/ProtocolSoup/internal/core"
"github.com/ParleSec/ProtocolSoup/internal/gateway"
"github.com/ParleSec/ProtocolSoup/internal/palette"
)

func main() {
cfg := core.LoadConfig()

var paletteSvc *palette.Service
if cfg.PaletteDBPath != "" {
svc, err := palette.NewService(cfg.PaletteDBPath)
if err != nil {
if cfg.IsProduction() {
log.Fatalf("Failed to load palette index at %s: %v", cfg.PaletteDBPath, err)
}
log.Printf("Palette service disabled: %v", err)
} else {
paletteSvc = svc
stats := svc.Stats()
log.Printf("Palette service initialized from %s (%d artefacts, index v%s)",
cfg.PaletteDBPath, stats.ArtefactCount, stats.IndexVersion)
}
}

upstreams := []gateway.UpstreamConfig{
{Name: "federation", BaseURL: strings.TrimSpace(os.Getenv("FEDERATION_SERVICE_URL"))},
{Name: "scim", BaseURL: strings.TrimSpace(os.Getenv("SCIM_SERVICE_URL"))},
Expand All @@ -33,6 +50,7 @@ func main() {
StartupRetryInitial: envDuration("GATEWAY_STARTUP_RETRY_INITIAL", 2*time.Second),
StartupRetryMax: envDuration("GATEWAY_STARTUP_RETRY_MAX", 30*time.Second),
RequestTimeout: envDuration("GATEWAY_REQUEST_TIMEOUT", 5*time.Second),
Palette: paletteSvc,
})
if err != nil {
log.Fatalf("Failed to initialize gateway: %v", err)
Expand Down
13 changes: 12 additions & 1 deletion backend/internal/gateway/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"sync"
"time"

"github.com/ParleSec/ProtocolSoup/internal/palette"
"github.com/go-chi/chi/v5"
"github.com/go-chi/cors"
)
Expand All @@ -29,6 +30,9 @@ type Config struct {
StartupRetryInitial time.Duration
StartupRetryMax time.Duration
RequestTimeout time.Duration
// Palette serves POST /api/palette/query from a local SQLite index. Nil
// omits the route (split deployments without a baked index).
Palette *palette.Service
}

// UpstreamConfig represents an upstream service.
Expand Down Expand Up @@ -66,6 +70,8 @@ type Upstream struct {
type Gateway struct {
cfg Config

palette *palette.Service

client *http.Client
upstreams map[string]*Upstream
order []string
Expand Down Expand Up @@ -119,7 +125,8 @@ func NewGateway(cfg Config) (*Gateway, error) {
}

return &Gateway{
cfg: cfg,
cfg: cfg,
palette: cfg.Palette,
client: &http.Client{
Timeout: cfg.RequestTimeout,
},
Expand Down Expand Up @@ -161,6 +168,10 @@ func (g *Gateway) Router() http.Handler {
r.Post("/lookingglass/decode", g.handleDecodeToken)
r.Get("/lookingglass/sessions", g.handleListSessions)
r.Get("/lookingglass/sessions/{id}", g.handleGetSession)

if g.palette != nil {
r.Post("/palette/query", g.palette.Handler())
}
})

r.Get("/ws/lookingglass/{session}", g.handleLookingGlassWS)
Expand Down
3 changes: 3 additions & 0 deletions backend/internal/gateway/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ func (g *Gateway) handleAPIIndex(w http.ResponseWriter, r *http.Request) {
"lookingglass": "/api/lookingglass",
"lookingglass_ws": "/ws/lookingglass/{session}",
}
if g.palette != nil {
endpoints["palette"] = "/api/palette/query"
}

g.writeJSON(w, http.StatusOK, apiIndexResponse{
Service: "protocol-lens-gateway",
Expand Down
6 changes: 5 additions & 1 deletion docker/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Protocol Soup - Split Backend Services (Base)
# Run with: docker compose up -d
# Run with: docker compose up -d --build
# After pulling new code, always pass --build so frontend and gateway images
# are rebuilt from source (palette UI and /api/palette/query are baked in).
#
# This starts:
# - Gateway (API aggregation + protocol routing)
Expand Down Expand Up @@ -45,6 +47,7 @@ services:
- SHOWCASE_LISTEN_ADDR=:8080
- SHOWCASE_BASE_URL=http://localhost:8080
- SHOWCASE_CORS_ORIGINS=http://localhost:3000,http://localhost:5173
- SHOWCASE_PALETTE_DB=/app/palette.db
- FEDERATION_SERVICE_URL=http://federation-service:8080
- SCIM_SERVICE_URL=http://scim-service:8080
- SSF_SERVICE_URL=http://ssf-service:8080
Expand Down Expand Up @@ -177,6 +180,7 @@ services:
NEXT_PUBLIC_WS_BASE_URL: ws://localhost:8080
# For using published GHCR images, comment 'build:' above and uncomment:
# image: ghcr.io/parlesec/protocolsoup-frontend:latest
image: protocol-lens-frontend:latest
container_name: protocol-showcase-frontend
ports:
- "3000:3000"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ Run the complete ProtocolSoup experience with all services.

```bash
cd docker
docker compose up -d
docker compose up -d --build
```

**Services started:** frontend, gateway, federation-service, scim-service, ssf-service

The gateway image ships the [palette content index](/deploy/palette-index/) at `/app/palette.db` and serves `POST /api/palette/query` for homepage search and **cmd+K**. Rebuild images after pulling new code (`--build`); `docker compose up -d` alone reuses cached layers and can leave you on an older UI.

**Add SPIFFE:**

```bash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Run the frontend, gateway, federation, SCIM, and SSF services together.

```bash
cd docker
docker compose up -d
docker compose up -d --build
```

This starts five services:
Expand Down
154 changes: 82 additions & 72 deletions frontend/src/components/common/LayoutHeader.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,85 +56,108 @@ export function LayoutHeader() {
<>
<header className="fixed top-0 left-0 right-0 z-50 bg-surface-950/80 backdrop-blur-sm border-b border-white/5">
<div className="max-w-5xl mx-auto px-3 sm:px-6 py-2.5 sm:py-3">
<div className="flex items-center justify-between gap-2">
<Link href="/" className="flex items-center gap-2 sm:gap-2.5 group min-w-0">
<span className="text-lg sm:text-xl flex-shrink-0">🍜</span>
<span className="font-semibold text-white group-hover:text-amber-100 transition-colors text-sm sm:text-base truncate">Protocol Soup</span>
</Link>
<div className="flex items-center gap-2">
<div className="flex-1 flex items-center">
<Link href="/" className="flex items-center gap-2 sm:gap-2.5 group flex-shrink-0">
<span className="text-lg sm:text-xl flex-shrink-0">🍜</span>
<span className="font-semibold text-white group-hover:text-amber-100 transition-colors text-sm sm:text-base whitespace-nowrap">Protocol Soup</span>
</Link>
</div>

<nav className="hidden lg:flex items-center gap-1">
{showSearchChip && (
<button
type="button"
onClick={openPalette}
aria-haspopup="dialog"
aria-label="Open search palette"
className="flex items-center gap-2 px-3 py-1.5 rounded-md transition-colors text-sm text-surface-400 hover:text-white hover:bg-white/5 mr-1"
>
<Search className="w-4 h-4" />
<span>Search</span>
{shortcutLabel && (
<kbd className="ml-1 hidden xl:inline-flex items-center rounded border border-white/10 bg-surface-800/60 px-1.5 py-0.5 text-[10px] font-mono text-surface-300">
{shortcutLabel}
</kbd>
)}
</button>
)}
<nav className="hidden lg:flex items-center gap-0.5 flex-shrink-0">
{navItems.map((item) => {
const isActive = currentPath === item.path ||
(item.path !== '/' && currentPath.startsWith(item.path))
return (
<Link
key={item.path}
href={item.path}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md transition-colors text-sm ${
aria-current={isActive ? 'page' : undefined}
className={`flex items-center gap-2 px-2.5 py-1.5 rounded-md transition-colors text-sm whitespace-nowrap flex-shrink-0 ${
isActive
? 'bg-white/10 text-white'
: 'text-surface-400 hover:text-white hover:bg-white/5'
}`}
>
<item.icon className="w-4 h-4" />
<item.icon className="w-4 h-4 flex-shrink-0" />
<span>{item.label}</span>
</Link>
)
})}
<div className="w-px h-4 bg-white/10 mx-2" />
<a
href="https://docs.protocolsoup.com"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-surface-400 hover:text-white hover:bg-white/5 transition-colors text-sm"
>
<FileText className="w-4 h-4" />
<span>Docs</span>
<ExternalLink className="w-3 h-3" />
</a>
<a
href="https://github.com/ParleSec/ProtocolSoup"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-surface-400 hover:text-white hover:bg-white/5 transition-colors text-sm"
>
<Github className="w-4 h-4" />
<span>Source</span>
<ExternalLink className="w-3 h-3" />
</a>
</nav>

<button
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="lg:hidden p-2 -mr-2 rounded-lg text-surface-400 hover:text-white hover:bg-white/5 transition-colors"
aria-label={isMobileMenuOpen ? 'Close menu' : 'Open menu'}
aria-haspopup="dialog"
aria-expanded={isMobileMenuOpen}
aria-controls="mobile-nav-drawer"
>
{isMobileMenuOpen ? (
<X className="w-6 h-6" />
) : (
<Menu className="w-6 h-6" />
<div className="flex-1 min-w-0 flex items-center justify-end gap-1">
<div className="hidden lg:flex items-center gap-1 min-w-0">
{showSearchChip && (
<button
type="button"
onClick={openPalette}
aria-haspopup="dialog"
aria-label="Search protocols and docs"
title="Search (press the shortcut to open)"
className="flex items-center justify-center h-9 w-9 rounded-md text-surface-400 hover:text-white hover:bg-white/5 transition-colors flex-shrink-0 xl:w-auto xl:justify-start xl:gap-2 xl:px-3 xl:border xl:border-white/10 xl:bg-surface-900/40 xl:hover:bg-surface-900/70"
>
<Search className="w-4 h-4 flex-shrink-0" />
<span className="hidden xl:inline text-sm">Search</span>
{shortcutLabel && (
<kbd className="hidden 2xl:inline-flex items-center rounded border border-white/10 bg-surface-800/60 px-1.5 py-0.5 text-[10px] font-mono text-surface-300">
{shortcutLabel}
</kbd>
)}
</button>
)}
<div className="w-px h-5 bg-white/10 mx-1 flex-shrink-0" />
<a
href="https://docs.protocolsoup.com"
target="_blank"
rel="noopener noreferrer"
aria-label="Documentation (opens in a new tab)"
title="Documentation"
className="flex items-center justify-center h-9 w-9 rounded-md text-surface-400 hover:text-white hover:bg-white/5 transition-colors flex-shrink-0 xl:w-auto xl:justify-start xl:gap-1.5 xl:px-2.5"
>
<FileText className="w-4 h-4 flex-shrink-0" />
<span className="hidden xl:inline text-sm">Docs</span>
</a>
<a
href="https://github.com/ParleSec/ProtocolSoup"
target="_blank"
rel="noopener noreferrer"
aria-label="Source code on GitHub (opens in a new tab)"
title="Source on GitHub"
className="flex items-center justify-center h-9 w-9 rounded-md text-surface-400 hover:text-white hover:bg-white/5 transition-colors flex-shrink-0 xl:w-auto xl:justify-start xl:gap-1.5 xl:px-2.5"
>
<Github className="w-4 h-4 flex-shrink-0" />
<span className="hidden xl:inline text-sm">Source</span>
</a>
</div>

{/* Mobile controls */}
{showSearchChip && (
<button
type="button"
onClick={openPalette}
className="lg:hidden p-2 rounded-lg text-surface-400 hover:text-white hover:bg-white/5 transition-colors"
aria-haspopup="dialog"
aria-label="Search protocols and docs"
>
<Search className="w-6 h-6" />
</button>
)}
</button>
<button
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="lg:hidden p-2 -mr-2 rounded-lg text-surface-400 hover:text-white hover:bg-white/5 transition-colors"
aria-label={isMobileMenuOpen ? 'Close menu' : 'Open menu'}
aria-haspopup="dialog"
aria-expanded={isMobileMenuOpen}
aria-controls="mobile-nav-drawer"
>
{isMobileMenuOpen ? (
<X className="w-6 h-6" />
) : (
<Menu className="w-6 h-6" />
)}
</button>
</div>
</div>
</div>
</header>
Expand Down Expand Up @@ -166,19 +189,6 @@ export function LayoutHeader() {
</div>

<div className="flex-1 overflow-y-auto p-4 space-y-1">
{showSearchChip && (
<button
type="button"
onClick={() => {
setIsMobileMenuOpen(false)
openPalette()
}}
className="flex w-full items-center gap-3 px-4 py-3 rounded-xl text-surface-400 hover:text-white hover:bg-white/5 transition-colors"
>
<Search className="w-5 h-5" />
<span className="font-medium">Search</span>
</button>
)}
{navItems.map((item) => {
const isActive = currentPath === item.path ||
(item.path !== '/' && currentPath.startsWith(item.path))
Expand Down
Loading