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
23 changes: 23 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Database
DB_HOST=127.0.0.1
DB_PORT=27017
DB_NAME=tracker
DB_USERNAME=
DB_PASSWORD=

# Server
HTTP_PORT=8080
GRPC_PORT=8765
LOG_LEVEL=info

# Jira
JIRA_DOMAIN=your-domain.atlassian.net
JIRA_PROJECT_KEY=

# Slack
SLACK_WORKSPACE=your-workspace
SLACK_EVENTS_CHANNEL=

# Homer Dashboard Integration (optional)
# URL of your Homer dashboard to import links from
HOMER_URL=
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ web/node_modules/
web/.env
web/.env.local

# Environment files (may contain secrets)
.env
.env.local

# Coverage reports
coverage.out
*.coverprofile
Expand Down
14 changes: 12 additions & 2 deletions cmd/serv.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ var serv = &cobra.Command{
panic(err)
}

// Register Homer proxy endpoint
server.RegisterHomerHandler(mux, os.Getenv("HOMER_URL"))

// Register custom links CRUD endpoints
server.RegisterLinksHandler(mux)

// Setup Swagger documentation with go-swagger
opts := middleware.SwaggerUIOpts{SpecURL: "/swagger.json"}
sh := middleware.SwaggerUI(opts, nil)
Expand Down Expand Up @@ -135,12 +141,15 @@ var serv = &cobra.Command{
buyMeCoffeeURL = "https://buymeacoffee.com/jplanckeel"
}

homerURL := os.Getenv("HOMER_URL")

// Escape values to prevent XSS injection
jiraDomain = html.EscapeString(jiraDomain)
jiraProjectKey = html.EscapeString(jiraProjectKey)
slackWorkspace = html.EscapeString(slackWorkspace)
slackEventsChannel = html.EscapeString(slackEventsChannel)
buyMeCoffeeURL = html.EscapeString(buyMeCoffeeURL)
homerURL = html.EscapeString(homerURL)

w.Header().Set("Content-Type", "application/javascript")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
Expand All @@ -156,8 +165,9 @@ var serv = &cobra.Command{
eventsChannel: "%s"
},
demoMode: %s,
buyMeCoffeeUrl: "%s"
};`, jiraDomain, jiraProjectKey, slackWorkspace, slackEventsChannel, demoMode, buyMeCoffeeURL)
buyMeCoffeeUrl: "%s",
homerUrl: "%s"
};`, jiraDomain, jiraProjectKey, slackWorkspace, slackEventsChannel, demoMode, buyMeCoffeeURL, homerURL)
if err != nil {
slog.Error("Failed to write config.js response", "error", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
Expand Down
134 changes: 134 additions & 0 deletions docs/LINKS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# 🔗 Links Page

La page Links est un tableau de bord de liens rapides inspiré de [Homer](https://github.com/bastienwirtz/homer). Elle centralise tous vos outils et ressources en un seul endroit.

## Fonctionnalités

- **Liens personnalisés** : Ajoutez, éditez et supprimez des liens stockés en base MongoDB
- **Intégration Homer** : Importez automatiquement les liens depuis un dashboard Homer existant
- **Recherche rapide** : Filtrez les liens inline ou via `Ctrl+K` depuis n'importe quelle page
- **Groupes** : Organisez vos liens par catégories
- **Favicons automatiques** : Les icônes sont récupérées automatiquement via Google S2 API
- **Icônes Font Awesome** : Supportées via la classe CSS (ex: `fas fa-rocket`)

## Ajouter des Liens Manuellement

1. Cliquer sur **Add Link** en haut à droite de la page
2. Remplir le formulaire :
- **Name** : Nom affiché du lien
- **URL** : URL complète (ex: `https://grafana.example.com`)
- **Group** : Catégorie (existante ou nouvelle)
- **Description** : Texte secondaire optionnel
- **Icon** : Classe Font Awesome (ex: `fas fa-chart-bar`) ou laisser vide pour le favicon auto
- **Logo URL** : URL d'une image logo (prioritaire sur l'icône)
- **Color** : Couleur de fond de l'icône (si pas de logo/favicon)
3. Cliquer **Save**

### Éditer / Supprimer

Au survol d'un lien personnalisé, les boutons crayon et poubelle apparaissent à droite.

## Intégration Homer

Homer est un dashboard de liens open source. Tracker peut importer ses liens automatiquement.

### Configuration Backend

Définir la variable d'environnement `HOMER_URL` avec l'URL de votre instance Homer :

```bash
# .env
HOMER_URL=http://homer.example.com
```

Le backend expose ensuite `/api/homer-links` qui proxy le fichier `config.yml` de Homer et retourne les liens et services.

### Configuration Frontend (développement)

En mode développement, le frontend utilise un proxy Vite pour éviter les problèmes CORS :

```bash
# web/.env.local
VITE_HOMER_URL=http://homer.example.com
```

```typescript
// web/vite.config.ts — proxy automatiquement configuré
'/homer-proxy': {
target: process.env.VITE_HOMER_URL,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/homer-proxy/, '')
}
```

### Configuration Helm

```yaml
# values.yaml
env:
homer:
url: "http://homer.example.com"
```

### Format du config.yml Homer

Tracker supporte le format standard Homer :

```yaml
# config.yml Homer
links:
- name: GitHub
url: https://github.com
icon: fab fa-github

services:
- name: Monitoring
items:
- name: Grafana
url: https://grafana.example.com
subtitle: Metrics & dashboards
icon: fas fa-chart-bar
- name: Prometheus
url: https://prometheus.example.com
subtitle: Alerting
icon: fas fa-bell
```

Les liens Homer sont affichés avec un badge **homer** orange pour les distinguer des liens personnalisés. Ils ne peuvent pas être édités depuis Tracker (modifier directement le `config.yml` Homer).

## Recherche Rapide (Ctrl+K)

Le raccourci `Ctrl+K` (ou `⌘K` sur Mac) ouvre une palette de recherche accessible depuis n'importe quelle page. Elle recherche simultanément dans :

- Les liens (locaux, Homer, personnalisés)
- Les services du catalogue

Utiliser les flèches `↑↓` pour naviguer, `Entrée` pour ouvrir, `Échap` pour fermer.

## Icônes Font Awesome

Font Awesome 6 Free est chargé via CDN. Utiliser les classes CSS directement :

```
fas fa-rocket # Solid icons
fab fa-github # Brand icons
far fa-calendar # Regular icons
```

Référence complète : https://fontawesome.com/icons

## Architecture Technique

```
Frontend (Links.tsx)
├── Liens locaux (config.ts)
├── Liens Homer (/api/homer-links ou /homer-proxy en dev)
└── Liens MongoDB (/api/links)

Backend
├── server/homer.go → GET /api/homer-links (proxy Homer config.yml)
└── server/links.go → CRUD /api/links (MongoDB)

Store
└── internal/stores/links.go → Collection MongoDB "links"
```
4 changes: 4 additions & 0 deletions helm/tracker/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ spec:
- name: SLACK_EVENTS_CHANNEL
value: {{ .Values.env.slack.eventsChannel | quote }}
{{- end }}
{{- if .Values.env.homer.url }}
- name: HOMER_URL
value: {{ .Values.env.homer.url | quote }}
{{- end }}
command:
- ./tracker
- serv
Expand Down
4 changes: 4 additions & 0 deletions helm/tracker/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ env:
workspace: "your-workspace"
# Slack channel ID for deployment events (e.g., C01234ABCDE)
eventsChannel: ""
homer:
# URL of your Homer dashboard to import links from (optional)
# Example: http://homer.example.com
url: ""

image:
repository: bananaops/tracker
Expand Down
22 changes: 22 additions & 0 deletions internal/stores/indexes.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ func EnsureIndexes(ctx context.Context, db *mongo.Database) error {
return err
}

// Index pour la collection links
if err := ensureLinksIndexes(ctx, db, logger); err != nil {
return err
}

logger.Info("All database indexes ensured successfully")
return nil
}
Expand Down Expand Up @@ -170,6 +175,23 @@ func ensureCatalogIndexes(ctx context.Context, db *mongo.Database, logger *slog.
return createIndexes(ctx, collection, indexes, logger, "catalogs")
}

func ensureLinksIndexes(ctx context.Context, db *mongo.Database, logger *slog.Logger) error {
collection := db.Collection("links")

indexes := []mongo.IndexModel{
{
Keys: bson.D{{Key: "group", Value: 1}},
Options: options.Index().SetName("idx_links_group"),
},
{
Keys: bson.D{{Key: "created_at", Value: -1}},
Options: options.Index().SetName("idx_links_created_at"),
},
}

return createIndexes(ctx, collection, indexes, logger, "links")
}

func createIndexes(ctx context.Context, collection *mongo.Collection, indexes []mongo.IndexModel, logger *slog.Logger, collectionName string) error {
// Créer un contexte avec timeout pour éviter les blocages
ctxTimeout, cancel := context.WithTimeout(ctx, 30*time.Second)
Expand Down
99 changes: 99 additions & 0 deletions internal/stores/links.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package store

import (
"context"
"fmt"
"time"

"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)

// LinkItem represents a single link stored in MongoDB
type LinkItem struct {
ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
Group string `bson:"group" json:"group"`
Name string `bson:"name" json:"name"`
URL string `bson:"url" json:"url"`
Description string `bson:"description,omitempty" json:"description,omitempty"`
Icon string `bson:"icon,omitempty" json:"icon,omitempty"`
Color string `bson:"color,omitempty" json:"color,omitempty"`
Logo string `bson:"logo,omitempty" json:"logo,omitempty"`
CreatedAt int64 `bson:"created_at" json:"created_at"`
UpdatedAt int64 `bson:"updated_at" json:"updated_at"`
}

type LinksStoreClient struct {
collection *mongo.Collection
}

func NewStoreLinks(collection string) *LinksStoreClient {
return &LinksStoreClient{
collection: NewClient(collection),
}
}

func (c *LinksStoreClient) List(ctx context.Context) ([]*LinkItem, error) {
opts := options.Find().SetSort(bson.D{{Key: "group", Value: 1}, {Key: "name", Value: 1}})
cursor, err := c.collection.Find(ctx, bson.D{}, opts)
if err != nil {
return nil, fmt.Errorf("failed to list links: %w", err)
}
var results []*LinkItem
if err := cursor.All(ctx, &results); err != nil {
return nil, fmt.Errorf("failed to decode links: %w", err)
}
return results, nil
}

func (c *LinksStoreClient) Create(ctx context.Context, item *LinkItem) (*LinkItem, error) {
now := time.Now().Unix()
item.ID = primitive.NewObjectID()
item.CreatedAt = now
item.UpdatedAt = now
_, err := c.collection.InsertOne(ctx, item)
if err != nil {
return nil, fmt.Errorf("failed to create link: %w", err)
}
return item, nil
}

func (c *LinksStoreClient) Update(ctx context.Context, id string, item *LinkItem) (*LinkItem, error) {
oid, err := primitive.ObjectIDFromHex(id)
if err != nil {
return nil, fmt.Errorf("invalid link id %s: %w", id, err)
}
item.UpdatedAt = time.Now().Unix()
filter := bson.D{{Key: "_id", Value: oid}}
update := bson.D{{Key: "$set", Value: bson.D{
{Key: "group", Value: item.Group},
{Key: "name", Value: item.Name},
{Key: "url", Value: item.URL},
{Key: "description", Value: item.Description},
{Key: "icon", Value: item.Icon},
{Key: "color", Value: item.Color},
{Key: "logo", Value: item.Logo},
{Key: "updated_at", Value: item.UpdatedAt},
}}}
opts := options.FindOneAndUpdate().SetReturnDocument(options.After)
var result LinkItem
if err := c.collection.FindOneAndUpdate(ctx, filter, update, opts).Decode(&result); err != nil {
return nil, fmt.Errorf("failed to update link %s: %w", id, err)
}
return &result, nil
}

func (c *LinksStoreClient) Delete(ctx context.Context, id string) error {
oid, err := primitive.ObjectIDFromHex(id)
if err != nil {
return fmt.Errorf("invalid link id %s: %w", id, err)
}
filter := bson.D{{Key: "_id", Value: oid}}
_, err = c.collection.DeleteOne(ctx, filter)
if err != nil {
return fmt.Errorf("failed to delete link %s: %w", id, err)
}
return nil
}
1 change: 1 addition & 0 deletions node_modules/.bin/baseline-browser-mapping

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions node_modules/.package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading