-
Notifications
You must be signed in to change notification settings - Fork 0
feat: OAuth SSO, decoder programming, and wizard drive support #59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,8 @@ import ( | |
| "encoding/json" | ||
| stderrors "errors" | ||
|
|
||
| "github.com/sirupsen/logrus" | ||
|
|
||
| "github.com/keskad/loco/pkgs/bigfred/contract" | ||
| "github.com/keskad/loco/pkgs/bigfred/dcc-bus/protocol" | ||
| "github.com/keskad/loco/pkgs/bigfred/dcc-bus/service" | ||
|
|
@@ -48,6 +50,63 @@ func (r *Router) HandleControlCommand(ctx context.Context, raw []byte) { | |
| return | ||
| } | ||
| r.applyEStopTarget(ctx, p.Addresses) | ||
|
|
||
| case protocol.TypeLocoCVWrite: | ||
| var p protocol.LocoCVWritePayload | ||
| if err := json.Unmarshal(env.Payload, &p); err != nil { | ||
| return | ||
| } | ||
| r.logControlProgramming(env.Type, r.HandleLocoCVWrite(ctx, controlActor, noopResponder{}, p, "")) | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This means a server-side caller (e.g. a future admin "program all decoders" batch) has no way to know its CV write was rejected because programming was disabled on the target daemon. Consider publishing a result frame back on a |
||
|
|
||
| case protocol.TypeLocoCVRead: | ||
| var p protocol.LocoCVReadPayload | ||
| if err := json.Unmarshal(env.Payload, &p); err != nil { | ||
| return | ||
| } | ||
| r.logControlProgramming(env.Type, r.HandleLocoCVRead(ctx, controlActor, noopResponder{}, p, "")) | ||
|
|
||
| case protocol.TypeLocoAddrSet: | ||
| var p protocol.LocoAddrSetPayload | ||
| if err := json.Unmarshal(env.Payload, &p); err != nil { | ||
| return | ||
| } | ||
| r.logControlProgramming(env.Type, r.HandleLocoAddrSet(ctx, controlActor, noopResponder{}, p, "")) | ||
|
|
||
| case protocol.TypeLocoAddrGet: | ||
| var p protocol.LocoAddrGetPayload | ||
| if err := json.Unmarshal(env.Payload, &p); err != nil { | ||
| return | ||
| } | ||
| r.logControlProgramming(env.Type, r.HandleLocoAddrGet(ctx, controlActor, noopResponder{}, p, "")) | ||
| } | ||
| } | ||
|
|
||
| // controlActor labels commands that arrive on the Redis control channel | ||
| // rather than from a browser session. | ||
| var controlActor = Actor{Source: "server"} | ||
|
|
||
| // logControlProgramming reports the outcome of a control-channel | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✨ [POSITIVE] POSITIVE: The |
||
| // programming command. The channel is fire-and-forget, so on rejection | ||
| // the daemon publishes a control.programming.rejected event on its | ||
| // event channel — that is the server's only signal that the command | ||
| // did not run (loco-server can log it, surface it to an admin HUD, or | ||
| // retry against a different station). On success the daemon log is | ||
| // enough; no event is emitted. | ||
| func (r *Router) logControlProgramming(frameType string, res Result) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✨ [POSITIVE] POSITIVE: The |
||
| if res.OK { | ||
| return | ||
| } | ||
| r.log.WithFields(logrus.Fields{ | ||
| "type": frameType, | ||
| "code": res.Code, | ||
| }).Warn("dcc-bus control programming command rejected") | ||
| if r.redis != nil { | ||
| _ = r.redis.Publish(context.Background(), protocol.TypeControlProgrammingRejected, | ||
| protocol.ControlProgrammingRejectedPayload{ | ||
| FrameType: frameType, | ||
| Code: res.Code, | ||
| Address: res.LocoAddress, | ||
| }) | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| package cmd | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "io" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/alicebob/miniredis/v2" | ||
| "github.com/redis/go-redis/v9" | ||
| "github.com/sirupsen/logrus" | ||
|
|
||
| "github.com/keskad/loco/pkgs/bigfred/contract" | ||
| buserrors "github.com/keskad/loco/pkgs/bigfred/dcc-bus/errors" | ||
| "github.com/keskad/loco/pkgs/bigfred/dcc-bus/protocol" | ||
| "github.com/keskad/loco/pkgs/bigfred/dcc-bus/state" | ||
| ) | ||
|
|
||
| func TestLogControlProgramming_publishesRejectionEvent(t *testing.T) { | ||
| t.Parallel() | ||
| mr, err := miniredis.Run() | ||
| if err != nil { | ||
| t.Fatalf("miniredis: %v", err) | ||
| } | ||
| defer mr.Close() | ||
|
|
||
| rs := state.NewRedis(redis.NewClient(&redis.Options{Addr: mr.Addr()}), 2, 1) | ||
| log := logrus.New() | ||
| log.SetOutput(io.Discard) | ||
| r := &Router{redis: rs, log: log} | ||
|
|
||
| // Subscribe to the daemon's event channel before publishing so the | ||
| // message is not lost to fire-and-forget timing. | ||
| ctx, cancel := context.WithCancel(context.Background()) | ||
| defer cancel() | ||
| sub := rs.Client().Subscribe(ctx, contract.DccBusEventChannel(2, 1)) | ||
| defer func() { _ = sub.Close() }() | ||
| if _, err := sub.Receive(ctx); err != nil { | ||
| t.Fatalf("subscribe: %v", err) | ||
| } | ||
| msgCh := sub.Channel() | ||
|
|
||
| r.logControlProgramming(protocol.TypeLocoCVWrite, Result{ | ||
| OK: false, | ||
| Code: buserrors.CodeProgrammingDisabled, | ||
| LocoAddress: 47, | ||
| }) | ||
|
|
||
| select { | ||
| case msg := <-msgCh: | ||
| var env contract.EnvelopeWire | ||
| if err := json.Unmarshal([]byte(msg.Payload), &env); err != nil { | ||
| t.Fatalf("unmarshal envelope: %v", err) | ||
| } | ||
| if env.Type != protocol.TypeControlProgrammingRejected { | ||
| t.Fatalf("event type = %q, want %q", env.Type, protocol.TypeControlProgrammingRejected) | ||
| } | ||
| var p protocol.ControlProgrammingRejectedPayload | ||
| if err := json.Unmarshal(env.Payload, &p); err != nil { | ||
| t.Fatalf("unmarshal payload: %v", err) | ||
| } | ||
| if p.FrameType != protocol.TypeLocoCVWrite { | ||
| t.Errorf("frameType = %q, want %q", p.FrameType, protocol.TypeLocoCVWrite) | ||
| } | ||
| if p.Code != buserrors.CodeProgrammingDisabled { | ||
| t.Errorf("code = %q, want %q", p.Code, buserrors.CodeProgrammingDisabled) | ||
| } | ||
| if p.Address != 47 { | ||
| t.Errorf("address = %d, want 47", p.Address) | ||
| } | ||
| case <-time.After(time.Second): | ||
| t.Fatal("control.programming.rejected event was not published") | ||
| } | ||
| } | ||
|
|
||
| func TestLogControlProgramming_silentOnSuccess(t *testing.T) { | ||
| t.Parallel() | ||
| mr, err := miniredis.Run() | ||
| if err != nil { | ||
| t.Fatalf("miniredis: %v", err) | ||
| } | ||
| defer mr.Close() | ||
|
|
||
| rs := state.NewRedis(redis.NewClient(&redis.Options{Addr: mr.Addr()}), 2, 1) | ||
| log := logrus.New() | ||
| log.SetOutput(io.Discard) | ||
| r := &Router{redis: rs, log: log} | ||
|
|
||
| ctx, cancel := context.WithCancel(context.Background()) | ||
| defer cancel() | ||
| sub := rs.Client().Subscribe(ctx, contract.DccBusEventChannel(2, 1)) | ||
| defer func() { _ = sub.Close() }() | ||
| if _, err := sub.Receive(ctx); err != nil { | ||
| t.Fatalf("subscribe: %v", err) | ||
| } | ||
| msgCh := sub.Channel() | ||
|
|
||
| r.logControlProgramming(protocol.TypeLocoCVWrite, OKResult()) | ||
|
|
||
| select { | ||
| case msg := <-msgCh: | ||
| t.Fatalf("expected no event on success, got %s", msg.Payload) | ||
| case <-time.After(150 * time.Millisecond): | ||
| // ok — no event published. | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,6 +44,25 @@ type Responder interface { | |
| SendAck(ctx context.Context, requestID string, payload protocol.AckPayload) error | ||
| } | ||
|
|
||
| // noopResponder satisfies Responder for commands that arrive without a | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✨ [POSITIVE] POSITIVE: The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✨ [POSITIVE] POSITIVE: The |
||
| // client to answer — the Redis control channel is fire-and-forget. | ||
| type noopResponder struct{} | ||
|
|
||
| func (noopResponder) Subscribe(...uint16) {} | ||
| func (noopResponder) Unsubscribe(...uint16) {} | ||
| func (noopResponder) SubscribedAddrs() []uint16 { return nil } | ||
| func (noopResponder) OldestSubscribed() (uint16, bool) { return 0, false } | ||
| func (noopResponder) SelectedAddr() uint16 { return 0 } | ||
| func (noopResponder) SetSelected(uint16) {} | ||
| func (noopResponder) ClearSelected() {} | ||
|
|
||
| func (noopResponder) SendLocoState(context.Context, contract.LocoStateWire) error { return nil } | ||
| func (noopResponder) SendLocoError(context.Context, uint16, string, string) error { return nil } | ||
| func (noopResponder) SendLocoErrorPayload(context.Context, protocol.LocoErrorPayload) error { | ||
| return nil | ||
| } | ||
| func (noopResponder) SendAck(context.Context, string, protocol.AckPayload) error { return nil } | ||
|
|
||
| // SessionView is a snapshot of one live browser session used for fan-out | ||
| // and dead-man bookkeeping without importing the ws package. | ||
| type SessionView struct { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✨ [POSITIVE] POSITIVE: The
ProgrammingTrackFromFlagfunction provides clear validation and normalization for the programming track configuration, improving robustness and user experience.