Matrix-Chatfenster (read-only) im Dashboard integrieren

This commit is contained in:
Kai
2026-02-17 15:42:58 +01:00
parent 53d4052446
commit 234bd7e182
9 changed files with 207 additions and 2 deletions

View File

@@ -3,3 +3,9 @@ APP_PORT=8080
# Beispiel fuer externe MariaDB im Netzwerk: # Beispiel fuer externe MariaDB im Netzwerk:
# DB_DSN=farmcal:DEIN_PASSWORT@tcp(192.168.178.20:3306)/farmcal?parseTime=true # DB_DSN=farmcal:DEIN_PASSWORT@tcp(192.168.178.20:3306)/farmcal?parseTime=true
DB_DSN=farmcal:CHANGE_ME@tcp(127.0.0.1:3306)/farmcal?parseTime=true DB_DSN=farmcal:CHANGE_ME@tcp(127.0.0.1:3306)/farmcal?parseTime=true
# Optional: Matrix-Chat (read-only)
# MATRIX_HOMESERVER_URL=https://matrix.example.com
# MATRIX_ACCESS_TOKEN=YOUR_MATRIX_ACCESS_TOKEN
# MATRIX_ROOM_ID=!abcdefg12345:example.com
# MATRIX_LIMIT=25

View File

@@ -34,6 +34,18 @@ APP_PORT=8080
DB_DSN=farmcal:SEHR_SICHERES_PASSWORT@tcp(192.168.1.40:3306)/farmcal?parseTime=true DB_DSN=farmcal:SEHR_SICHERES_PASSWORT@tcp(192.168.1.40:3306)/farmcal?parseTime=true
``` ```
Optional fuer Matrix-Chat (nur Anzeige):
```env
MATRIX_HOMESERVER_URL=https://matrix.example.com
MATRIX_ACCESS_TOKEN=YOUR_MATRIX_ACCESS_TOKEN
MATRIX_ROOM_ID=!abcdefg12345:example.com
MATRIX_LIMIT=25
```
- `MATRIX_ACCESS_TOKEN` sollte ein User-Token mit Leserechten fuer den Raum sein.
- Wenn Matrix-Variablen fehlen, bleibt das Feature deaktiviert.
## Start mit Docker Compose ## Start mit Docker Compose
```bash ```bash
@@ -95,3 +107,4 @@ git push -u origin main
- Kalenderansicht fuer mehrere Monate - Kalenderansicht fuer mehrere Monate
- Mehrspieler-/Benutzerverwaltung - Mehrspieler-/Benutzerverwaltung
- Import/Export fuer bestehende Tabellen - Import/Export fuer bestehende Tabellen
- Matrix: Nachrichten aus FarmCal senden (write support)

View File

@@ -69,6 +69,16 @@ func (a *App) handleDashboard(w http.ResponseWriter, r *http.Request) {
Calendar: buildCalendar(plans, crops, products, stepMap, customTasks, doneMap, (settings.CurrentCycle*12)+(settings.CurrentMonth-1), settings.CurrentDay, settings.DaysPerMonth, 14), Calendar: buildCalendar(plans, crops, products, stepMap, customTasks, doneMap, (settings.CurrentCycle*12)+(settings.CurrentMonth-1), settings.CurrentDay, settings.DaysPerMonth, 14),
PlanningCount: len(plans), PlanningCount: len(plans),
} }
if a.matrix != nil {
data.MatrixEnabled = true
data.MatrixRoomID = a.matrix.RoomID
msgs, err := a.matrix.FetchRecentMessages(r.Context())
if err != nil {
data.MatrixError = err.Error()
} else {
data.MatrixMessages = msgs
}
}
data.TodayGroups = groupTasksByField(data.TodayTasks) data.TodayGroups = groupTasksByField(data.TodayTasks)
a.renderTemplate(w, "templates/dashboard.html", data) a.renderTemplate(w, "templates/dashboard.html", data)
} }

View File

@@ -18,7 +18,8 @@ var monthNames = []string{
} }
type App struct { type App struct {
db *sql.DB db *sql.DB
matrix *MatrixClient
} }
func main() { func main() {
@@ -38,7 +39,10 @@ func main() {
log.Fatalf("schema setup failed: %v", err) log.Fatalf("schema setup failed: %v", err)
} }
app := &App{db: db} app := &App{
db: db,
matrix: newMatrixClientFromEnv(),
}
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc("/", app.handleDashboard) mux.HandleFunc("/", app.handleDashboard)

115
cmd/server/matrix.go Normal file
View File

@@ -0,0 +1,115 @@
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
type MatrixClient struct {
HomeserverURL string
AccessToken string
RoomID string
Limit int
httpClient *http.Client
}
func newMatrixClientFromEnv() *MatrixClient {
hs := strings.TrimRight(readEnv("MATRIX_HOMESERVER_URL", ""), "/")
token := readEnv("MATRIX_ACCESS_TOKEN", "")
roomID := readEnv("MATRIX_ROOM_ID", "")
if hs == "" || token == "" || roomID == "" {
return nil
}
limit := mustInt(readEnv("MATRIX_LIMIT", "25"), 25)
if limit < 1 {
limit = 1
}
if limit > 100 {
limit = 100
}
return &MatrixClient{
HomeserverURL: hs,
AccessToken: token,
RoomID: roomID,
Limit: limit,
httpClient: &http.Client{
Timeout: 6 * time.Second,
},
}
}
func (m *MatrixClient) FetchRecentMessages(ctx context.Context) ([]MatrixMessage, error) {
filter := fmt.Sprintf(`{"room":{"rooms":["%s"],"timeline":{"limit":%d}}}`, m.RoomID, m.Limit)
endpoint := m.HomeserverURL + "/_matrix/client/v3/sync?timeout=0&filter=" + url.QueryEscape(filter)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+m.AccessToken)
resp, err := m.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, errors.New("matrix sync failed: " + strconv.Itoa(resp.StatusCode))
}
var syncResp struct {
Rooms struct {
Join map[string]struct {
Timeline struct {
Events []struct {
Type string `json:"type"`
Sender string `json:"sender"`
OriginServerTS int64 `json:"origin_server_ts"`
Content struct {
MsgType string `json:"msgtype"`
Body string `json:"body"`
} `json:"content"`
} `json:"events"`
} `json:"timeline"`
} `json:"join"`
} `json:"rooms"`
}
if err := json.NewDecoder(resp.Body).Decode(&syncResp); err != nil {
return nil, err
}
room, ok := syncResp.Rooms.Join[m.RoomID]
if !ok {
return []MatrixMessage{}, nil
}
out := make([]MatrixMessage, 0, len(room.Timeline.Events))
for _, ev := range room.Timeline.Events {
if ev.Type != "m.room.message" {
continue
}
if ev.Content.MsgType != "m.text" && ev.Content.MsgType != "m.notice" {
continue
}
body := strings.TrimSpace(ev.Content.Body)
if body == "" {
continue
}
ts := time.UnixMilli(ev.OriginServerTS).Local().Format("02.01.2006 15:04")
out = append(out, MatrixMessage{
Sender: ev.Sender,
Body: body,
Timestamp: ts,
})
}
return out, nil
}

View File

@@ -149,6 +149,10 @@ type DashboardPage struct {
TodayGroups []FieldTaskGroup TodayGroups []FieldTaskGroup
Calendar []CalendarMonth Calendar []CalendarMonth
PlanningCount int PlanningCount int
MatrixEnabled bool
MatrixRoomID string
MatrixError string
MatrixMessages []MatrixMessage
} }
type FieldsPage struct { type FieldsPage struct {
@@ -181,3 +185,9 @@ type GeneralPage struct {
Settings Settings Settings Settings
Months []MonthOption Months []MonthOption
} }
type MatrixMessage struct {
Sender string
Body string
Timestamp string
}

View File

@@ -9,3 +9,7 @@ services:
environment: environment:
APP_ADDR: ":8080" APP_ADDR: ":8080"
DB_DSN: "${DB_DSN}" DB_DSN: "${DB_DSN}"
MATRIX_HOMESERVER_URL: "${MATRIX_HOMESERVER_URL:-}"
MATRIX_ACCESS_TOKEN: "${MATRIX_ACCESS_TOKEN:-}"
MATRIX_ROOM_ID: "${MATRIX_ROOM_ID:-}"
MATRIX_LIMIT: "${MATRIX_LIMIT:-25}"

View File

@@ -250,6 +250,25 @@ button:hover { background: linear-gradient(180deg, #7cbc19, #5e8a12); }
.month-days li { margin: .24rem 0; } .month-days li { margin: .24rem 0; }
.matrix-list {
list-style: none;
margin: .5rem 0 0;
padding: 0;
display: grid;
gap: .5rem;
max-height: 280px;
overflow: auto;
}
.matrix-list li {
border: 1px solid #d7decb;
border-radius: 9px;
background: #f9fdf2;
padding: .5rem .6rem;
display: grid;
gap: .2rem;
}
.table-wrap { .table-wrap {
overflow-x: auto; overflow-x: auto;
max-width: 100%; max-width: 100%;

View File

@@ -83,6 +83,30 @@
{{end}} {{end}}
</section> </section>
<section class="card">
<h2>Matrix-Chat</h2>
{{if .MatrixEnabled}}
<p class="hint">Raum: <code>{{.MatrixRoomID}}</code></p>
{{if .MatrixError}}
<p class="muted">Chat konnte nicht geladen werden: {{.MatrixError}}</p>
{{else if .MatrixMessages}}
<ul class="matrix-list">
{{range .MatrixMessages}}
<li>
<div><strong>{{.Sender}}</strong></div>
<div>{{.Body}}</div>
<div class="muted">{{.Timestamp}}</div>
</li>
{{end}}
</ul>
{{else}}
<p class="muted">Keine Nachrichten gefunden.</p>
{{end}}
{{else}}
<p class="muted">Matrix-Chat ist nicht konfiguriert.</p>
{{end}}
</section>
<section class="card full-width"> <section class="card full-width">
<h2>Kalender ab jetzt (14 Monate)</h2> <h2>Kalender ab jetzt (14 Monate)</h2>
<div class="calendar-grid"> <div class="calendar-grid">