Matrix-Chatfenster (read-only) im Dashboard integrieren
This commit is contained in:
@@ -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
|
||||||
|
|||||||
13
README.md
13
README.md
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
115
cmd/server/matrix.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|||||||
@@ -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%;
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user