diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index f52da3fa5d..abb9bde232 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -403,7 +403,7 @@ module.exports = {
     'github/a11y-svg-has-accessible-name': [0],
     'github/array-foreach': [0],
     'github/async-currenttarget': [2],
-    'github/async-preventdefault': [2],
+    'github/async-preventdefault': [0], // https://github.com/github/eslint-plugin-github/issues/599
     'github/authenticity-token': [0],
     'github/get-attribute': [0],
     'github/js-class-name': [0],
diff --git a/cmd/web.go b/cmd/web.go
index f8217758e5..dc5c6de48a 100644
--- a/cmd/web.go
+++ b/cmd/web.go
@@ -18,10 +18,12 @@ import (
 
 	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/graceful"
+	"code.gitea.io/gitea/modules/gtprof"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/process"
 	"code.gitea.io/gitea/modules/public"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers"
 	"code.gitea.io/gitea/routers/install"
 
@@ -218,6 +220,8 @@ func serveInstalled(ctx *cli.Context) error {
 		}
 	}
 
+	gtprof.EnableBuiltinTracer(util.Iif(setting.IsProd, 2000*time.Millisecond, 100*time.Millisecond))
+
 	// Set up Chi routes
 	webRoutes := routers.NormalRoutes()
 	err := listen(webRoutes, true)
diff --git a/flake.lock b/flake.lock
index 1890b82dcf..4319737c26 100644
--- a/flake.lock
+++ b/flake.lock
@@ -5,11 +5,11 @@
         "systems": "systems"
       },
       "locked": {
-        "lastModified": 1726560853,
-        "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
+        "lastModified": 1731533236,
+        "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
         "owner": "numtide",
         "repo": "flake-utils",
-        "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
+        "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
         "type": "github"
       },
       "original": {
@@ -20,11 +20,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1731139594,
-        "narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
+        "lastModified": 1736798957,
+        "narHash": "sha256-qwpCtZhSsSNQtK4xYGzMiyEDhkNzOCz/Vfu4oL2ETsQ=",
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
+        "rev": "9abb87b552b7f55ac8916b6fc9e5cb486656a2f3",
         "type": "github"
       },
       "original": {
diff --git a/flake.nix b/flake.nix
index e3655b627e..f54eba1c3b 100644
--- a/flake.nix
+++ b/flake.nix
@@ -29,9 +29,14 @@
             poetry
 
             # backend
+            go_1_23
             gofumpt
             sqlite
           ];
+          shellHook = ''
+            export GO="${pkgs.go_1_23}/bin/go"
+            export GOROOT="${pkgs.go_1_23}/share/go"
+          '';
         };
       }
     );
diff --git a/models/db/engine_hook.go b/models/db/engine_hook.go
index b4c543c3dd..2c9fc09c99 100644
--- a/models/db/engine_hook.go
+++ b/models/db/engine_hook.go
@@ -7,23 +7,36 @@ import (
 	"context"
 	"time"
 
+	"code.gitea.io/gitea/modules/gtprof"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
 
 	"xorm.io/xorm/contexts"
 )
 
-type SlowQueryHook struct {
+type EngineHook struct {
 	Threshold time.Duration
 	Logger    log.Logger
 }
 
-var _ contexts.Hook = (*SlowQueryHook)(nil)
+var _ contexts.Hook = (*EngineHook)(nil)
 
-func (*SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
-	return c.Ctx, nil
+func (*EngineHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
+	ctx, _ := gtprof.GetTracer().Start(c.Ctx, gtprof.TraceSpanDatabase)
+	return ctx, nil
 }
 
-func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error {
+func (h *EngineHook) AfterProcess(c *contexts.ContextHook) error {
+	span := gtprof.GetContextSpan(c.Ctx)
+	if span != nil {
+		// Do not record SQL parameters here:
+		// * It shouldn't expose the parameters because they contain sensitive information, end users need to report the trace details safely.
+		// * Some parameters contain quite long texts, waste memory and are difficult to display.
+		span.SetAttributeString(gtprof.TraceAttrDbSQL, c.SQL)
+		span.End()
+	} else {
+		setting.PanicInDevOrTesting("span in database engine hook is nil")
+	}
 	if c.ExecuteTime >= h.Threshold {
 		// 8 is the amount of skips passed to runtime.Caller, so that in the log the correct function
 		// is being displayed (the function that ultimately wants to execute the query in the code)
diff --git a/models/db/engine_init.go b/models/db/engine_init.go
index da85018957..edca697934 100644
--- a/models/db/engine_init.go
+++ b/models/db/engine_init.go
@@ -72,7 +72,7 @@ func InitEngine(ctx context.Context) error {
 	xe.SetDefaultContext(ctx)
 
 	if setting.Database.SlowQueryThreshold > 0 {
-		xe.AddHook(&SlowQueryHook{
+		xe.AddHook(&EngineHook{
 			Threshold: setting.Database.SlowQueryThreshold,
 			Logger:    log.GetLogger("xorm"),
 		})
diff --git a/models/fixtures/access.yml b/models/fixtures/access.yml
index 4171e31fef..596046e950 100644
--- a/models/fixtures/access.yml
+++ b/models/fixtures/access.yml
@@ -171,3 +171,9 @@
   user_id: 40
   repo_id: 61
   mode: 4
+
+-
+  id: 30
+  user_id: 40
+  repo_id: 1
+  mode: 2
diff --git a/models/git/branch.go b/models/git/branch.go
index e683ce47e6..d1caa35947 100644
--- a/models/git/branch.go
+++ b/models/git/branch.go
@@ -167,6 +167,9 @@ func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, e
 			BranchName: branchName,
 		}
 	}
+	// FIXME: this design is not right: it doesn't check `branch.IsDeleted`, it doesn't make sense to make callers to check IsDeleted again and again.
+	// It causes inconsistency with `GetBranches` and `git.GetBranch`, and will lead to strange bugs
+	// In the future, there should be 2 functions: `GetBranchExisting` and `GetBranchWithDeleted`
 	return &branch, nil
 }
 
@@ -440,6 +443,8 @@ type FindRecentlyPushedNewBranchesOptions struct {
 }
 
 type RecentlyPushedNewBranch struct {
+	BranchRepo        *repo_model.Repository
+	BranchName        string
 	BranchDisplayName string
 	BranchLink        string
 	BranchCompareURL  string
@@ -540,7 +545,9 @@ func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, o
 				branchDisplayName = fmt.Sprintf("%s:%s", branch.Repo.FullName(), branchDisplayName)
 			}
 			newBranches = append(newBranches, &RecentlyPushedNewBranch{
+				BranchRepo:        branch.Repo,
 				BranchDisplayName: branchDisplayName,
+				BranchName:        branch.Name,
 				BranchLink:        fmt.Sprintf("%s/src/branch/%s", branch.Repo.Link(), util.PathEscapeSegments(branch.Name)),
 				BranchCompareURL:  branch.Repo.ComposeBranchCompareURL(opts.BaseRepo, branch.Name),
 				CommitTime:        branch.CommitTime,
diff --git a/models/issues/stopwatch.go b/models/issues/stopwatch.go
index 629af95b57..7c05a3a883 100644
--- a/models/issues/stopwatch.go
+++ b/models/issues/stopwatch.go
@@ -46,11 +46,6 @@ func (s Stopwatch) Seconds() int64 {
 	return int64(timeutil.TimeStampNow() - s.CreatedUnix)
 }
 
-// Duration returns a human-readable duration string based on local server time
-func (s Stopwatch) Duration() string {
-	return util.SecToTime(s.Seconds())
-}
-
 func getStopwatch(ctx context.Context, userID, issueID int64) (sw *Stopwatch, exists bool, err error) {
 	sw = new(Stopwatch)
 	exists, err = db.GetEngine(ctx).
@@ -201,7 +196,7 @@ func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Iss
 		Doer:    user,
 		Issue:   issue,
 		Repo:    issue.Repo,
-		Content: util.SecToTime(timediff),
+		Content: util.SecToHours(timediff),
 		Type:    CommentTypeStopTracking,
 		TimeID:  tt.ID,
 	}); err != nil {
diff --git a/modules/git/command.go b/modules/git/command.go
index 2584e3cc57..602d00f027 100644
--- a/modules/git/command.go
+++ b/modules/git/command.go
@@ -18,6 +18,7 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/modules/git/internal" //nolint:depguard // only this file can use the internal type CmdArg, other files and packages should use AddXxx functions
+	"code.gitea.io/gitea/modules/gtprof"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/process"
 	"code.gitea.io/gitea/modules/util"
@@ -54,7 +55,7 @@ func logArgSanitize(arg string) string {
 	} else if filepath.IsAbs(arg) {
 		base := filepath.Base(arg)
 		dir := filepath.Dir(arg)
-		return filepath.Join(filepath.Base(dir), base)
+		return ".../" + filepath.Join(filepath.Base(dir), base)
 	}
 	return arg
 }
@@ -295,15 +296,20 @@ func (c *Command) run(skip int, opts *RunOpts) error {
 		timeout = defaultCommandExecutionTimeout
 	}
 
-	var desc string
+	cmdLogString := c.LogString()
 	callerInfo := util.CallerFuncName(1 /* util */ + 1 /* this */ + skip /* parent */)
 	if pos := strings.LastIndex(callerInfo, "/"); pos >= 0 {
 		callerInfo = callerInfo[pos+1:]
 	}
 	// these logs are for debugging purposes only, so no guarantee of correctness or stability
-	desc = fmt.Sprintf("git.Run(by:%s, repo:%s): %s", callerInfo, logArgSanitize(opts.Dir), c.LogString())
+	desc := fmt.Sprintf("git.Run(by:%s, repo:%s): %s", callerInfo, logArgSanitize(opts.Dir), cmdLogString)
 	log.Debug("git.Command: %s", desc)
 
+	_, span := gtprof.GetTracer().Start(c.parentContext, gtprof.TraceSpanGitRun)
+	defer span.End()
+	span.SetAttributeString(gtprof.TraceAttrFuncCaller, callerInfo)
+	span.SetAttributeString(gtprof.TraceAttrGitCommand, cmdLogString)
+
 	var ctx context.Context
 	var cancel context.CancelFunc
 	var finished context.CancelFunc
diff --git a/modules/git/command_test.go b/modules/git/command_test.go
index 0823afd7f7..e988714db7 100644
--- a/modules/git/command_test.go
+++ b/modules/git/command_test.go
@@ -58,5 +58,5 @@ func TestCommandString(t *testing.T) {
 	assert.EqualValues(t, cmd.prog+` a "-m msg" "it's a test" "say \"hello\""`, cmd.LogString())
 
 	cmd = NewCommandContextNoGlobals(context.Background(), "url: https://a:b@c/", "/root/dir-a/dir-b")
-	assert.EqualValues(t, cmd.prog+` "url: https://sanitized-credential@c/" dir-a/dir-b`, cmd.LogString())
+	assert.EqualValues(t, cmd.prog+` "url: https://sanitized-credential@c/" .../dir-a/dir-b`, cmd.LogString())
 }
diff --git a/modules/git/diff.go b/modules/git/diff.go
index 833f6220f9..da0a2f26ba 100644
--- a/modules/git/diff.go
+++ b/modules/git/diff.go
@@ -64,7 +64,10 @@ func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diff
 		} else if commit.ParentCount() == 0 {
 			cmd.AddArguments("show").AddDynamicArguments(endCommit).AddDashesAndList(files...)
 		} else {
-			c, _ := commit.Parent(0)
+			c, err := commit.Parent(0)
+			if err != nil {
+				return err
+			}
 			cmd.AddArguments("diff", "-M").AddDynamicArguments(c.ID.String(), endCommit).AddDashesAndList(files...)
 		}
 	case RawDiffPatch:
@@ -74,7 +77,10 @@ func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diff
 		} else if commit.ParentCount() == 0 {
 			cmd.AddArguments("format-patch", "--no-signature", "--stdout", "--root").AddDynamicArguments(endCommit).AddDashesAndList(files...)
 		} else {
-			c, _ := commit.Parent(0)
+			c, err := commit.Parent(0)
+			if err != nil {
+				return err
+			}
 			query := fmt.Sprintf("%s...%s", endCommit, c.ID.String())
 			cmd.AddArguments("format-patch", "--no-signature", "--stdout").AddDynamicArguments(query).AddDashesAndList(files...)
 		}
diff --git a/modules/git/repo_branch_gogit.go b/modules/git/repo_branch_gogit.go
index dbc4a5fedc..77aecb21eb 100644
--- a/modules/git/repo_branch_gogit.go
+++ b/modules/git/repo_branch_gogit.go
@@ -57,7 +57,7 @@ func (repo *Repository) IsBranchExist(name string) bool {
 
 // GetBranches returns branches from the repository, skipping "skip" initial branches and
 // returning at most "limit" branches, or all branches if "limit" is 0.
-// Branches are returned with sort of `-commiterdate` as the nogogit
+// Branches are returned with sort of `-committerdate` as the nogogit
 // implementation. This requires full fetch, sort and then the
 // skip/limit applies later as gogit returns in undefined order.
 func (repo *Repository) GetBranchNames(skip, limit int) ([]string, int, error) {
diff --git a/modules/gtprof/event.go b/modules/gtprof/event.go
new file mode 100644
index 0000000000..da4a0faff9
--- /dev/null
+++ b/modules/gtprof/event.go
@@ -0,0 +1,32 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package gtprof
+
+type EventConfig struct {
+	attributes []*TraceAttribute
+}
+
+type EventOption interface {
+	applyEvent(*EventConfig)
+}
+
+type applyEventFunc func(*EventConfig)
+
+func (f applyEventFunc) applyEvent(cfg *EventConfig) {
+	f(cfg)
+}
+
+func WithAttributes(attrs ...*TraceAttribute) EventOption {
+	return applyEventFunc(func(cfg *EventConfig) {
+		cfg.attributes = append(cfg.attributes, attrs...)
+	})
+}
+
+func eventConfigFromOptions(options ...EventOption) *EventConfig {
+	cfg := &EventConfig{}
+	for _, opt := range options {
+		opt.applyEvent(cfg)
+	}
+	return cfg
+}
diff --git a/modules/gtprof/trace.go b/modules/gtprof/trace.go
new file mode 100644
index 0000000000..ad67c226dc
--- /dev/null
+++ b/modules/gtprof/trace.go
@@ -0,0 +1,175 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package gtprof
+
+import (
+	"context"
+	"fmt"
+	"sync"
+	"time"
+
+	"code.gitea.io/gitea/modules/util"
+)
+
+type contextKey struct {
+	name string
+}
+
+var contextKeySpan = &contextKey{"span"}
+
+type traceStarter interface {
+	start(ctx context.Context, traceSpan *TraceSpan, internalSpanIdx int) (context.Context, traceSpanInternal)
+}
+
+type traceSpanInternal interface {
+	addEvent(name string, cfg *EventConfig)
+	recordError(err error, cfg *EventConfig)
+	end()
+}
+
+type TraceSpan struct {
+	// immutable
+	parent           *TraceSpan
+	internalSpans    []traceSpanInternal
+	internalContexts []context.Context
+
+	// mutable, must be protected by mutex
+	mu         sync.RWMutex
+	name       string
+	statusCode uint32
+	statusDesc string
+	startTime  time.Time
+	endTime    time.Time
+	attributes []*TraceAttribute
+	children   []*TraceSpan
+}
+
+type TraceAttribute struct {
+	Key   string
+	Value TraceValue
+}
+
+type TraceValue struct {
+	v any
+}
+
+func (t *TraceValue) AsString() string {
+	return fmt.Sprint(t.v)
+}
+
+func (t *TraceValue) AsInt64() int64 {
+	v, _ := util.ToInt64(t.v)
+	return v
+}
+
+func (t *TraceValue) AsFloat64() float64 {
+	v, _ := util.ToFloat64(t.v)
+	return v
+}
+
+var globalTraceStarters []traceStarter
+
+type Tracer struct {
+	starters []traceStarter
+}
+
+func (s *TraceSpan) SetName(name string) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	s.name = name
+}
+
+func (s *TraceSpan) SetStatus(code uint32, desc string) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	s.statusCode, s.statusDesc = code, desc
+}
+
+func (s *TraceSpan) AddEvent(name string, options ...EventOption) {
+	cfg := eventConfigFromOptions(options...)
+	for _, tsp := range s.internalSpans {
+		tsp.addEvent(name, cfg)
+	}
+}
+
+func (s *TraceSpan) RecordError(err error, options ...EventOption) {
+	cfg := eventConfigFromOptions(options...)
+	for _, tsp := range s.internalSpans {
+		tsp.recordError(err, cfg)
+	}
+}
+
+func (s *TraceSpan) SetAttributeString(key, value string) *TraceSpan {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	s.attributes = append(s.attributes, &TraceAttribute{Key: key, Value: TraceValue{v: value}})
+	return s
+}
+
+func (t *Tracer) Start(ctx context.Context, spanName string) (context.Context, *TraceSpan) {
+	starters := t.starters
+	if starters == nil {
+		starters = globalTraceStarters
+	}
+	ts := &TraceSpan{name: spanName, startTime: time.Now()}
+	parentSpan := GetContextSpan(ctx)
+	if parentSpan != nil {
+		parentSpan.mu.Lock()
+		parentSpan.children = append(parentSpan.children, ts)
+		parentSpan.mu.Unlock()
+		ts.parent = parentSpan
+	}
+
+	parentCtx := ctx
+	for internalSpanIdx, tsp := range starters {
+		var internalSpan traceSpanInternal
+		if parentSpan != nil {
+			parentCtx = parentSpan.internalContexts[internalSpanIdx]
+		}
+		ctx, internalSpan = tsp.start(parentCtx, ts, internalSpanIdx)
+		ts.internalContexts = append(ts.internalContexts, ctx)
+		ts.internalSpans = append(ts.internalSpans, internalSpan)
+	}
+	ctx = context.WithValue(ctx, contextKeySpan, ts)
+	return ctx, ts
+}
+
+type mutableContext interface {
+	context.Context
+	SetContextValue(key, value any)
+	GetContextValue(key any) any
+}
+
+// StartInContext starts a trace span in Gitea's mutable context (usually the web request context).
+// Due to the design limitation of Gitea's web framework, it can't use `context.WithValue` to bind a new span into a new context.
+// So here we use our "reqctx" framework to achieve the same result: web request context could always see the latest "span".
+func (t *Tracer) StartInContext(ctx mutableContext, spanName string) (*TraceSpan, func()) {
+	curTraceSpan := GetContextSpan(ctx)
+	_, newTraceSpan := GetTracer().Start(ctx, spanName)
+	ctx.SetContextValue(contextKeySpan, newTraceSpan)
+	return newTraceSpan, func() {
+		newTraceSpan.End()
+		ctx.SetContextValue(contextKeySpan, curTraceSpan)
+	}
+}
+
+func (s *TraceSpan) End() {
+	s.mu.Lock()
+	s.endTime = time.Now()
+	s.mu.Unlock()
+
+	for _, tsp := range s.internalSpans {
+		tsp.end()
+	}
+}
+
+func GetTracer() *Tracer {
+	return &Tracer{}
+}
+
+func GetContextSpan(ctx context.Context) *TraceSpan {
+	ts, _ := ctx.Value(contextKeySpan).(*TraceSpan)
+	return ts
+}
diff --git a/modules/gtprof/trace_builtin.go b/modules/gtprof/trace_builtin.go
new file mode 100644
index 0000000000..41743a25e4
--- /dev/null
+++ b/modules/gtprof/trace_builtin.go
@@ -0,0 +1,96 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package gtprof
+
+import (
+	"context"
+	"fmt"
+	"strings"
+	"sync/atomic"
+	"time"
+
+	"code.gitea.io/gitea/modules/tailmsg"
+)
+
+type traceBuiltinStarter struct{}
+
+type traceBuiltinSpan struct {
+	ts *TraceSpan
+
+	internalSpanIdx int
+}
+
+func (t *traceBuiltinSpan) addEvent(name string, cfg *EventConfig) {
+	// No-op because builtin tracer doesn't need it.
+	// In the future we might use it to mark the time point between backend logic and network response.
+}
+
+func (t *traceBuiltinSpan) recordError(err error, cfg *EventConfig) {
+	// No-op because builtin tracer doesn't need it.
+	// Actually Gitea doesn't handle err this way in most cases
+}
+
+func (t *traceBuiltinSpan) toString(out *strings.Builder, indent int) {
+	t.ts.mu.RLock()
+	defer t.ts.mu.RUnlock()
+
+	out.WriteString(strings.Repeat(" ", indent))
+	out.WriteString(t.ts.name)
+	if t.ts.endTime.IsZero() {
+		out.WriteString(" duration: (not ended)")
+	} else {
+		out.WriteString(fmt.Sprintf(" duration=%.4fs", t.ts.endTime.Sub(t.ts.startTime).Seconds()))
+	}
+	for _, a := range t.ts.attributes {
+		out.WriteString(" ")
+		out.WriteString(a.Key)
+		out.WriteString("=")
+		value := a.Value.AsString()
+		if strings.ContainsAny(value, " \t\r\n") {
+			quoted := false
+			for _, c := range "\"'`" {
+				if quoted = !strings.Contains(value, string(c)); quoted {
+					value = string(c) + value + string(c)
+					break
+				}
+			}
+			if !quoted {
+				value = fmt.Sprintf("%q", value)
+			}
+		}
+		out.WriteString(value)
+	}
+	out.WriteString("\n")
+	for _, c := range t.ts.children {
+		span := c.internalSpans[t.internalSpanIdx].(*traceBuiltinSpan)
+		span.toString(out, indent+2)
+	}
+}
+
+func (t *traceBuiltinSpan) end() {
+	if t.ts.parent == nil {
+		// TODO: debug purpose only
+		// TODO: it should distinguish between http response network lag and actual processing time
+		threshold := time.Duration(traceBuiltinThreshold.Load())
+		if threshold != 0 && t.ts.endTime.Sub(t.ts.startTime) > threshold {
+			sb := &strings.Builder{}
+			t.toString(sb, 0)
+			tailmsg.GetManager().GetTraceRecorder().Record(sb.String())
+		}
+	}
+}
+
+func (t *traceBuiltinStarter) start(ctx context.Context, traceSpan *TraceSpan, internalSpanIdx int) (context.Context, traceSpanInternal) {
+	return ctx, &traceBuiltinSpan{ts: traceSpan, internalSpanIdx: internalSpanIdx}
+}
+
+func init() {
+	globalTraceStarters = append(globalTraceStarters, &traceBuiltinStarter{})
+}
+
+var traceBuiltinThreshold atomic.Int64
+
+func EnableBuiltinTracer(threshold time.Duration) {
+	traceBuiltinThreshold.Store(int64(threshold))
+}
diff --git a/modules/gtprof/trace_const.go b/modules/gtprof/trace_const.go
new file mode 100644
index 0000000000..af9ce9223f
--- /dev/null
+++ b/modules/gtprof/trace_const.go
@@ -0,0 +1,19 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package gtprof
+
+// Some interesting names could be found in https://github.com/open-telemetry/opentelemetry-go/tree/main/semconv
+
+const (
+	TraceSpanHTTP     = "http"
+	TraceSpanGitRun   = "git-run"
+	TraceSpanDatabase = "database"
+)
+
+const (
+	TraceAttrFuncCaller = "func.caller"
+	TraceAttrDbSQL      = "db.sql"
+	TraceAttrGitCommand = "git.command"
+	TraceAttrHTTPRoute  = "http.route"
+)
diff --git a/modules/gtprof/trace_test.go b/modules/gtprof/trace_test.go
new file mode 100644
index 0000000000..7e1743c88d
--- /dev/null
+++ b/modules/gtprof/trace_test.go
@@ -0,0 +1,93 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package gtprof
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+// "vendor span" is a simple demo for a span from a vendor library
+
+var vendorContextKey any = "vendorContextKey"
+
+type vendorSpan struct {
+	name     string
+	children []*vendorSpan
+}
+
+func vendorTraceStart(ctx context.Context, name string) (context.Context, *vendorSpan) {
+	span := &vendorSpan{name: name}
+	parentSpan, ok := ctx.Value(vendorContextKey).(*vendorSpan)
+	if ok {
+		parentSpan.children = append(parentSpan.children, span)
+	}
+	ctx = context.WithValue(ctx, vendorContextKey, span)
+	return ctx, span
+}
+
+// below "testTrace*" integrate the vendor span into our trace system
+
+type testTraceSpan struct {
+	vendorSpan *vendorSpan
+}
+
+func (t *testTraceSpan) addEvent(name string, cfg *EventConfig) {}
+
+func (t *testTraceSpan) recordError(err error, cfg *EventConfig) {}
+
+func (t *testTraceSpan) end() {}
+
+type testTraceStarter struct{}
+
+func (t *testTraceStarter) start(ctx context.Context, traceSpan *TraceSpan, internalSpanIdx int) (context.Context, traceSpanInternal) {
+	ctx, span := vendorTraceStart(ctx, traceSpan.name)
+	return ctx, &testTraceSpan{span}
+}
+
+func TestTraceStarter(t *testing.T) {
+	globalTraceStarters = []traceStarter{&testTraceStarter{}}
+
+	ctx := context.Background()
+	ctx, span := GetTracer().Start(ctx, "root")
+	defer span.End()
+
+	func(ctx context.Context) {
+		ctx, span := GetTracer().Start(ctx, "span1")
+		defer span.End()
+		func(ctx context.Context) {
+			_, span := GetTracer().Start(ctx, "spanA")
+			defer span.End()
+		}(ctx)
+		func(ctx context.Context) {
+			_, span := GetTracer().Start(ctx, "spanB")
+			defer span.End()
+		}(ctx)
+	}(ctx)
+
+	func(ctx context.Context) {
+		_, span := GetTracer().Start(ctx, "span2")
+		defer span.End()
+	}(ctx)
+
+	var spanFullNames []string
+	var collectSpanNames func(parentFullName string, s *vendorSpan)
+	collectSpanNames = func(parentFullName string, s *vendorSpan) {
+		fullName := parentFullName + "/" + s.name
+		spanFullNames = append(spanFullNames, fullName)
+		for _, c := range s.children {
+			collectSpanNames(fullName, c)
+		}
+	}
+	collectSpanNames("", span.internalSpans[0].(*testTraceSpan).vendorSpan)
+	assert.Equal(t, []string{
+		"/root",
+		"/root/span1",
+		"/root/span1/spanA",
+		"/root/span1/spanB",
+		"/root/span2",
+	}, spanFullNames)
+}
diff --git a/modules/repository/branch.go b/modules/repository/branch.go
index 4630e70aa8..2bf9930f19 100644
--- a/modules/repository/branch.go
+++ b/modules/repository/branch.go
@@ -6,7 +6,6 @@ package repository
 import (
 	"context"
 	"fmt"
-	"strings"
 
 	"code.gitea.io/gitea/models/db"
 	git_model "code.gitea.io/gitea/models/git"
@@ -52,9 +51,6 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository,
 	{
 		branches, _, err := gitRepo.GetBranchNames(0, 0)
 		if err != nil {
-			if strings.Contains(err.Error(), "ref file is empty") {
-				return 0, nil
-			}
 			return 0, err
 		}
 		log.Trace("SyncRepoBranches[%s]: branches[%d]: %v", repo.FullName(), len(branches), branches)
diff --git a/modules/tailmsg/talimsg.go b/modules/tailmsg/talimsg.go
new file mode 100644
index 0000000000..aafc98e2d2
--- /dev/null
+++ b/modules/tailmsg/talimsg.go
@@ -0,0 +1,73 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package tailmsg
+
+import (
+	"sync"
+	"time"
+)
+
+type MsgRecord struct {
+	Time    time.Time
+	Content string
+}
+
+type MsgRecorder interface {
+	Record(content string)
+	GetRecords() []*MsgRecord
+}
+
+type memoryMsgRecorder struct {
+	mu    sync.RWMutex
+	msgs  []*MsgRecord
+	limit int
+}
+
+// TODO: use redis for a clustered environment
+
+func (m *memoryMsgRecorder) Record(content string) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	m.msgs = append(m.msgs, &MsgRecord{
+		Time:    time.Now(),
+		Content: content,
+	})
+	if len(m.msgs) > m.limit {
+		m.msgs = m.msgs[len(m.msgs)-m.limit:]
+	}
+}
+
+func (m *memoryMsgRecorder) GetRecords() []*MsgRecord {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+	ret := make([]*MsgRecord, len(m.msgs))
+	copy(ret, m.msgs)
+	return ret
+}
+
+func NewMsgRecorder(limit int) MsgRecorder {
+	return &memoryMsgRecorder{
+		limit: limit,
+	}
+}
+
+type Manager struct {
+	traceRecorder MsgRecorder
+	logRecorder   MsgRecorder
+}
+
+func (m *Manager) GetTraceRecorder() MsgRecorder {
+	return m.traceRecorder
+}
+
+func (m *Manager) GetLogRecorder() MsgRecorder {
+	return m.logRecorder
+}
+
+var GetManager = sync.OnceValue(func() *Manager {
+	return &Manager{
+		traceRecorder: NewMsgRecorder(100),
+		logRecorder:   NewMsgRecorder(1000),
+	}
+})
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index 609407d36b..a2cc166de9 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -69,7 +69,7 @@ func NewFuncMap() template.FuncMap {
 		// time / number / format
 		"FileSize": base.FileSize,
 		"CountFmt": countFmt,
-		"Sec2Time": util.SecToTime,
+		"Sec2Time": util.SecToHours,
 
 		"TimeEstimateString": timeEstimateString,
 
diff --git a/modules/util/sec_to_time.go b/modules/util/sec_to_time.go
index ad0fb1a68b..73667d723e 100644
--- a/modules/util/sec_to_time.go
+++ b/modules/util/sec_to_time.go
@@ -8,59 +8,17 @@ import (
 	"strings"
 )
 
-// SecToTime converts an amount of seconds to a human-readable string. E.g.
-// 66s			-> 1 minute 6 seconds
-// 52410s		-> 14 hours 33 minutes
-// 563418		-> 6 days 12 hours
-// 1563418		-> 2 weeks 4 days
-// 3937125s     -> 1 month 2 weeks
-// 45677465s	-> 1 year 6 months
-func SecToTime(durationVal any) string {
+// SecToHours converts an amount of seconds to a human-readable hours string.
+// This is stable for planning and managing timesheets.
+// Here it only supports hours and minutes, because a work day could contain 6 or 7 or 8 hours.
+func SecToHours(durationVal any) string {
 	duration, _ := ToInt64(durationVal)
+	hours := duration / 3600
+	minutes := (duration / 60) % 60
 
 	formattedTime := ""
-
-	// The following four variables are calculated by taking
-	// into account the previously calculated variables, this avoids
-	// pitfalls when using remainders. As that could lead to incorrect
-	// results when the calculated number equals the quotient number.
-	remainingDays := duration / (60 * 60 * 24)
-	years := remainingDays / 365
-	remainingDays -= years * 365
-	months := remainingDays * 12 / 365
-	remainingDays -= months * 365 / 12
-	weeks := remainingDays / 7
-	remainingDays -= weeks * 7
-	days := remainingDays
-
-	// The following three variables are calculated without depending
-	// on the previous calculated variables.
-	hours := (duration / 3600) % 24
-	minutes := (duration / 60) % 60
-	seconds := duration % 60
-
-	// Extract only the relevant information of the time
-	// If the time is greater than a year, it makes no sense to display seconds.
-	switch {
-	case years > 0:
-		formattedTime = formatTime(years, "year", formattedTime)
-		formattedTime = formatTime(months, "month", formattedTime)
-	case months > 0:
-		formattedTime = formatTime(months, "month", formattedTime)
-		formattedTime = formatTime(weeks, "week", formattedTime)
-	case weeks > 0:
-		formattedTime = formatTime(weeks, "week", formattedTime)
-		formattedTime = formatTime(days, "day", formattedTime)
-	case days > 0:
-		formattedTime = formatTime(days, "day", formattedTime)
-		formattedTime = formatTime(hours, "hour", formattedTime)
-	case hours > 0:
-		formattedTime = formatTime(hours, "hour", formattedTime)
-		formattedTime = formatTime(minutes, "minute", formattedTime)
-	default:
-		formattedTime = formatTime(minutes, "minute", formattedTime)
-		formattedTime = formatTime(seconds, "second", formattedTime)
-	}
+	formattedTime = formatTime(hours, "hour", formattedTime)
+	formattedTime = formatTime(minutes, "minute", formattedTime)
 
 	// The formatTime() function always appends a space at the end. This will be trimmed
 	return strings.TrimRight(formattedTime, " ")
@@ -76,6 +34,5 @@ func formatTime(value int64, name, formattedTime string) string {
 	} else if value > 1 {
 		formattedTime = fmt.Sprintf("%s%d %ss ", formattedTime, value, name)
 	}
-
 	return formattedTime
 }
diff --git a/modules/util/sec_to_time_test.go b/modules/util/sec_to_time_test.go
index 4d1213a52c..71a8801d4f 100644
--- a/modules/util/sec_to_time_test.go
+++ b/modules/util/sec_to_time_test.go
@@ -9,22 +9,17 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func TestSecToTime(t *testing.T) {
+func TestSecToHours(t *testing.T) {
 	second := int64(1)
 	minute := 60 * second
 	hour := 60 * minute
 	day := 24 * hour
-	year := 365 * day
 
-	assert.Equal(t, "1 minute 6 seconds", SecToTime(minute+6*second))
-	assert.Equal(t, "1 hour", SecToTime(hour))
-	assert.Equal(t, "1 hour", SecToTime(hour+second))
-	assert.Equal(t, "14 hours 33 minutes", SecToTime(14*hour+33*minute+30*second))
-	assert.Equal(t, "6 days 12 hours", SecToTime(6*day+12*hour+30*minute+18*second))
-	assert.Equal(t, "2 weeks 4 days", SecToTime((2*7+4)*day+2*hour+16*minute+58*second))
-	assert.Equal(t, "4 weeks", SecToTime(4*7*day))
-	assert.Equal(t, "4 weeks 1 day", SecToTime((4*7+1)*day))
-	assert.Equal(t, "1 month 2 weeks", SecToTime((6*7+3)*day+13*hour+38*minute+45*second))
-	assert.Equal(t, "11 months", SecToTime(year-25*day))
-	assert.Equal(t, "1 year 5 months", SecToTime(year+163*day+10*hour+11*minute+5*second))
+	assert.Equal(t, "1 minute", SecToHours(minute+6*second))
+	assert.Equal(t, "1 hour", SecToHours(hour))
+	assert.Equal(t, "1 hour", SecToHours(hour+second))
+	assert.Equal(t, "14 hours 33 minutes", SecToHours(14*hour+33*minute+30*second))
+	assert.Equal(t, "156 hours 30 minutes", SecToHours(6*day+12*hour+30*minute+18*second))
+	assert.Equal(t, "98 hours 16 minutes", SecToHours(4*day+2*hour+16*minute+58*second))
+	assert.Equal(t, "672 hours", SecToHours(4*7*day))
 }
diff --git a/modules/web/handler.go b/modules/web/handler.go
index 9a3e4a7f17..42a649714d 100644
--- a/modules/web/handler.go
+++ b/modules/web/handler.go
@@ -121,7 +121,7 @@ func wrapHandlerProvider[T http.Handler](hp func(next http.Handler) T, funcInfo
 	return func(next http.Handler) http.Handler {
 		h := hp(next) // this handle could be dynamically generated, so we can't use it for debug info
 		return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
-			routing.UpdateFuncInfo(req.Context(), funcInfo)
+			defer routing.RecordFuncInfo(req.Context(), funcInfo)()
 			h.ServeHTTP(resp, req)
 		})
 	}
@@ -157,7 +157,7 @@ func toHandlerProvider(handler any) func(next http.Handler) http.Handler {
 				return // it's doing pre-check, just return
 			}
 
-			routing.UpdateFuncInfo(req.Context(), funcInfo)
+			defer routing.RecordFuncInfo(req.Context(), funcInfo)()
 			ret := fn.Call(argsIn)
 
 			// handle the return value (no-op at the moment)
diff --git a/modules/web/routing/context.go b/modules/web/routing/context.go
index c5e85a415b..d3eb98f83d 100644
--- a/modules/web/routing/context.go
+++ b/modules/web/routing/context.go
@@ -6,22 +6,29 @@ package routing
 import (
 	"context"
 	"net/http"
+
+	"code.gitea.io/gitea/modules/gtprof"
+	"code.gitea.io/gitea/modules/reqctx"
 )
 
 type contextKeyType struct{}
 
 var contextKey contextKeyType
 
-// UpdateFuncInfo updates a context's func info
-func UpdateFuncInfo(ctx context.Context, funcInfo *FuncInfo) {
-	record, ok := ctx.Value(contextKey).(*requestRecord)
-	if !ok {
-		return
+// RecordFuncInfo records a func info into context
+func RecordFuncInfo(ctx context.Context, funcInfo *FuncInfo) (end func()) {
+	end = func() {}
+	if reqCtx := reqctx.FromContext(ctx); reqCtx != nil {
+		var traceSpan *gtprof.TraceSpan
+		traceSpan, end = gtprof.GetTracer().StartInContext(reqCtx, "http.func")
+		traceSpan.SetAttributeString("func", funcInfo.shortName)
 	}
-
-	record.lock.Lock()
-	record.funcInfo = funcInfo
-	record.lock.Unlock()
+	if record, ok := ctx.Value(contextKey).(*requestRecord); ok {
+		record.lock.Lock()
+		record.funcInfo = funcInfo
+		record.lock.Unlock()
+	}
+	return end
 }
 
 // MarkLongPolling marks the request is a long-polling request, and the logger may output different message for it
diff --git a/options/gitignore/Node b/options/gitignore/Node
index c6bba59138..1170717c14 100644
--- a/options/gitignore/Node
+++ b/options/gitignore/Node
@@ -104,6 +104,12 @@ dist
 .temp
 .cache
 
+# vitepress build output
+**/.vitepress/dist
+
+# vitepress cache directory
+**/.vitepress/cache
+
 # Docusaurus cache and generated files
 .docusaurus
 
diff --git a/options/gitignore/Python b/options/gitignore/Python
index 15201acc11..0a197900e2 100644
--- a/options/gitignore/Python
+++ b/options/gitignore/Python
@@ -167,5 +167,8 @@ cython_debug/
 #  option (not recommended) you can uncomment the following to ignore the entire idea folder.
 #.idea/
 
+# Ruff stuff:
+.ruff_cache/
+
 # PyPI configuration file
 .pypirc
diff --git a/options/gitignore/Rust b/options/gitignore/Rust
index d01bd1a990..0104787a73 100644
--- a/options/gitignore/Rust
+++ b/options/gitignore/Rust
@@ -3,10 +3,6 @@
 debug/
 target/
 
-# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
-# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
-Cargo.lock
-
 # These are backup files generated by rustfmt
 **/*.rs.bk
 
diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini
index 78e8db160c..7bbc509c23 100644
--- a/options/locale/locale_cs-CZ.ini
+++ b/options/locale/locale_cs-CZ.ini
@@ -1683,16 +1683,13 @@ issues.timetracker_timer_manually_add=Přidat čas
 
 issues.time_estimate_set=Nastavit odhadovaný čas
 issues.time_estimate_display=Odhad: %s
-issues.change_time_estimate_at=změnil/a odhad času na <b>%s</b> %s
 issues.remove_time_estimate_at=odstranil/a odhad času %s
 issues.time_estimate_invalid=Formát odhadu času je neplatný
 issues.start_tracking_history=započal/a práci %s
 issues.tracker_auto_close=Časovač se automaticky zastaví po zavření tohoto úkolu
 issues.tracking_already_started=`Již jste spustili sledování času na <a href="%s">jiném úkolu</a>!`
-issues.stop_tracking_history=pracoval/a <b>%s</b> %s
 issues.cancel_tracking_history=`zrušil/a sledování času %s`
 issues.del_time=Odstranit tento časový záznam
-issues.add_time_history=přidal/a strávený čas <b>%s</b> %s
 issues.del_time_history=`odstranil/a strávený čas %s`
 issues.add_time_manually=Přidat čas ručně
 issues.add_time_hours=Hodiny
@@ -3369,7 +3366,6 @@ monitor.execute_time=Doba provádění
 monitor.last_execution_result=Výsledek
 monitor.process.cancel=Zrušit proces
 monitor.process.cancel_desc=Zrušení procesu může způsobit ztrátu dat
-monitor.process.cancel_notices=Zrušit: <strong>%s</strong>?
 monitor.process.children=Potomek
 
 monitor.queues=Fronty
@@ -3566,7 +3562,6 @@ conda.install=Pro instalaci balíčku pomocí Conda spusťte následující př
 container.details.type=Typ obrazu
 container.details.platform=Platforma
 container.pull=Stáhněte obraz z příkazové řádky:
-container.digest=Výběr:
 container.multi_arch=OS/architektura
 container.layers=Vrstvy obrazů
 container.labels=Štítky
diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini
index 122bc1ec53..8fd2a7eda0 100644
--- a/options/locale/locale_de-DE.ini
+++ b/options/locale/locale_de-DE.ini
@@ -1678,16 +1678,13 @@ issues.timetracker_timer_manually_add=Zeit hinzufügen
 
 issues.time_estimate_set=Geschätzte Zeit festlegen
 issues.time_estimate_display=Schätzung: %s
-issues.change_time_estimate_at=Zeitschätzung geändert zu <b>%s</b> %s
 issues.remove_time_estimate_at=Zeitschätzung %s entfernt
 issues.time_estimate_invalid=Format der Zeitschätzung ist ungültig
 issues.start_tracking_history=hat die Zeiterfassung %s gestartet
 issues.tracker_auto_close=Der Timer wird automatisch gestoppt, wenn dieser Issue geschlossen wird
 issues.tracking_already_started=`Du hast die Zeiterfassung bereits in <a href="%s">diesem Issue</a> gestartet!`
-issues.stop_tracking_history=hat für <b>%s</b> gearbeitet %s
 issues.cancel_tracking_history=`hat die Zeiterfassung %s abgebrochen`
 issues.del_time=Diese Zeiterfassung löschen
-issues.add_time_history=hat <b>%s</b> gearbeitete Zeit hinzugefügt %s
 issues.del_time_history=`hat %s gearbeitete Zeit gelöscht`
 issues.add_time_manually=Zeit manuell hinzufügen
 issues.add_time_hours=Stunden
@@ -3359,7 +3356,6 @@ monitor.execute_time=Ausführungszeit
 monitor.last_execution_result=Ergebnis
 monitor.process.cancel=Prozess abbrechen
 monitor.process.cancel_desc=Abbrechen eines Prozesses kann Datenverlust verursachen
-monitor.process.cancel_notices=Abbrechen: <strong>%s</strong>?
 monitor.process.children=Subprozesse
 
 monitor.queues=Warteschlangen
@@ -3555,7 +3551,6 @@ conda.install=Um das Paket mit Conda zu installieren, führe den folgenden Befeh
 container.details.type=Container-Image Typ
 container.details.platform=Plattform
 container.pull=Downloade das Container-Image aus der Kommandozeile:
-container.digest=Digest:
 container.multi_arch=Betriebsystem / Architektur
 container.layers=Container-Image Ebenen
 container.labels=Labels
diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini
index af26256314..e989819c5e 100644
--- a/options/locale/locale_el-GR.ini
+++ b/options/locale/locale_el-GR.ini
@@ -3236,7 +3236,6 @@ conda.install=Για να εγκαταστήσετε το πακέτο χρησ
 container.details.type=Τύπος Εικόνας
 container.details.platform=Πλατφόρμα
 container.pull=Κατεβάστε την εικόνα από τη γραμμή εντολών:
-container.digest=Σύνοψη:
 container.multi_arch=ΛΣ / Αρχιτεκτονική
 container.layers=Στρώματα Εικόνας
 container.labels=Ετικέτες
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 0adc3193a0..ea39ddf837 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1690,16 +1690,16 @@ issues.timetracker_timer_manually_add = Add Time
 
 issues.time_estimate_set = Set estimated time
 issues.time_estimate_display = Estimate: %s
-issues.change_time_estimate_at = changed time estimate to <b>%s</b> %s
+issues.change_time_estimate_at = changed time estimate to <b>%[1]s</b> %[2]s
 issues.remove_time_estimate_at = removed time estimate %s
 issues.time_estimate_invalid = Time estimate format is invalid
 issues.start_tracking_history = started working %s
 issues.tracker_auto_close = Timer will be stopped automatically when this issue gets closed
 issues.tracking_already_started = `You have already started time tracking on <a href="%s">another issue</a>!`
-issues.stop_tracking_history = worked for <b>%s</b> %s
+issues.stop_tracking_history = worked for <b>%[1]s</b> %[2]s
 issues.cancel_tracking_history = `canceled time tracking %s`
 issues.del_time = Delete this time log
-issues.add_time_history = added spent time <b>%s</b> %s
+issues.add_time_history = added spent time <b>%[1]s</b> %[2]s
 issues.del_time_history= `deleted spent time %s`
 issues.add_time_manually = Manually Add Time
 issues.add_time_hours = Hours
@@ -1958,7 +1958,7 @@ pulls.upstream_diverging_prompt_behind_1 = This branch is %[1]d commit behind %[
 pulls.upstream_diverging_prompt_behind_n = This branch is %[1]d commits behind %[2]s
 pulls.upstream_diverging_prompt_base_newer = The base branch %s has new changes
 pulls.upstream_diverging_merge = Sync fork
-pulls.upstream_diverging_merge_confirm = Would you like to merge base repository's default branch onto this repository's branch %s?
+pulls.upstream_diverging_merge_confirm = Would you like to merge "%[1]s" onto "%[2]s"?
 
 pull.deleted_branch = (deleted):%s
 pull.agit_documentation = Review documentation about AGit
@@ -2719,6 +2719,8 @@ branch.create_branch_operation = Create branch
 branch.new_branch = Create new branch
 branch.new_branch_from = Create new branch from "%s"
 branch.renamed = Branch %s was renamed to %s.
+branch.rename_default_or_protected_branch_error = Only admins can rename default or protected branches.
+branch.rename_protected_branch_failed = This branch is protected by glob-based protection rules.
 
 tag.create_tag = Create tag %s
 tag.create_tag_operation = Create tag
@@ -3396,6 +3398,8 @@ monitor.previous = Previous Time
 monitor.execute_times = Executions
 monitor.process = Running Processes
 monitor.stacktrace = Stacktrace
+monitor.trace = Trace
+monitor.performance_logs = Performance Logs
 monitor.processes_count = %d Processes
 monitor.download_diagnosis_report = Download diagnosis report
 monitor.desc = Description
@@ -3404,7 +3408,6 @@ monitor.execute_time = Execution Time
 monitor.last_execution_result = Result
 monitor.process.cancel = Cancel process
 monitor.process.cancel_desc = Cancelling a process may cause data loss
-monitor.process.cancel_notices = Cancel: <strong>%s</strong>?
 monitor.process.children = Children
 
 monitor.queues = Queues
@@ -3601,7 +3604,8 @@ conda.install = To install the package using Conda, run the following command:
 container.details.type = Image Type
 container.details.platform = Platform
 container.pull = Pull the image from the command line:
-container.digest = Digest:
+container.images = Images
+container.digest = Digest
 container.multi_arch = OS / Arch
 container.layers = Image Layers
 container.labels = Labels
diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini
index e85e6b3399..049fb9196d 100644
--- a/options/locale/locale_es-ES.ini
+++ b/options/locale/locale_es-ES.ini
@@ -3215,7 +3215,6 @@ conda.install=Para instalar el paquete usando Conda, ejecute el siguiente comand
 container.details.type=Tipo de imagen
 container.details.platform=Plataforma
 container.pull=Arrastra la imagen desde la línea de comandos:
-container.digest=Resumen:
 container.multi_arch=SO / Arquitectura
 container.layers=Capas de imagen
 container.labels=Etiquetas
diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini
index bf31111b6e..0df4f5a00c 100644
--- a/options/locale/locale_fr-FR.ini
+++ b/options/locale/locale_fr-FR.ini
@@ -1683,16 +1683,13 @@ issues.timetracker_timer_manually_add=Pointer du temps
 
 issues.time_estimate_set=Définir le temps estimé
 issues.time_estimate_display=Estimation : %s
-issues.change_time_estimate_at=a changé le temps estimé à <b>%s</b> %s
 issues.remove_time_estimate_at=a supprimé le temps estimé %s
 issues.time_estimate_invalid=Le format du temps estimé est invalide
 issues.start_tracking_history=`a commencé son travail %s.`
 issues.tracker_auto_close=Le minuteur sera automatiquement arrêté quand le ticket sera fermé.
 issues.tracking_already_started=`Vous avez déjà un minuteur en cours sur <a href="%s">un autre ticket</a> !`
-issues.stop_tracking_history=`a fini de travailler sur <b>%s</b> %s.`
 issues.cancel_tracking_history=`a abandonné son minuteur %s.`
 issues.del_time=Supprimer ce minuteur du journal
-issues.add_time_history=`a pointé du temps de travail %s.`
 issues.del_time_history=`a supprimé son temps de travail %s.`
 issues.add_time_manually=Temps pointé manuellement
 issues.add_time_hours=Heures
@@ -3370,7 +3367,6 @@ monitor.execute_time=Heure d'Éxécution
 monitor.last_execution_result=Résultat
 monitor.process.cancel=Annuler le processus
 monitor.process.cancel_desc=L’annulation d’un processus peut entraîner une perte de données.
-monitor.process.cancel_notices=Annuler : <strong>%s</strong> ?
 monitor.process.children=Enfant
 
 monitor.queues=Files d'attente
@@ -3567,7 +3563,6 @@ conda.install=Pour installer le paquet en utilisant Conda, exécutez la commande
 container.details.type=Type d'image
 container.details.platform=Plateforme
 container.pull=Tirez l'image depuis un terminal :
-container.digest=Empreinte :
 container.multi_arch=SE / Arch
 container.layers=Calques d'image
 container.labels=Labels
diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini
index 3acdba35b0..9f63358a32 100644
--- a/options/locale/locale_ga-IE.ini
+++ b/options/locale/locale_ga-IE.ini
@@ -1684,16 +1684,13 @@ issues.timetracker_timer_manually_add=Cuir Am leis
 
 issues.time_estimate_set=Socraigh am measta
 issues.time_estimate_display=Meastachán: %s
-issues.change_time_estimate_at=d'athraigh an meastachán ama go <b>%s</b> %s
 issues.remove_time_estimate_at=baineadh meastachán ama %s
 issues.time_estimate_invalid=Tá formáid meastachán ama neamhbhailí
 issues.start_tracking_history=thosaigh ag obair %s
 issues.tracker_auto_close=Stopfar ama go huathoibríoch nuair a dhúnfar an tsaincheist seo
 issues.tracking_already_started=`Tá tús curtha agat cheana féin ag rianú ama ar <a href="%s">eagrán eile</a>!`
-issues.stop_tracking_history=d'oibrigh do <b>%s</b> %s
 issues.cancel_tracking_history=`rianú ama curtha ar ceal %s`
 issues.del_time=Scrios an log ama seo
-issues.add_time_history=cuireadh am caite <b>%s</b> %s leis
 issues.del_time_history=`an t-am caite scriosta %s`
 issues.add_time_manually=Cuir Am leis de Láimh
 issues.add_time_hours=Uaireanta
@@ -3371,7 +3368,6 @@ monitor.execute_time=Am Forghníomhaithe
 monitor.last_execution_result=Toradh
 monitor.process.cancel=Cealaigh próiseas
 monitor.process.cancel_desc=Má chuirtear próiseas ar ceal d'fhéadfadh go gcaillfí sonraí
-monitor.process.cancel_notices=Cealaigh: <strong>%s</strong>?
 monitor.process.children=Leanaí
 
 monitor.queues=Scuaineanna
@@ -3568,7 +3564,6 @@ conda.install=Chun an pacáiste a shuiteáil ag úsáid Conda, reáchtáil an t-
 container.details.type=Cineál Íomhá
 container.details.platform=Ardán
 container.pull=Tarraing an íomhá ón líne ordaithe:
-container.digest=Díleáigh:
 container.multi_arch=Córas Oibriúcháin / Ailtireacht
 container.layers=Sraitheanna Íomhá
 container.labels=Lipéid
diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini
index efe365dbfa..931e0523f9 100644
--- a/options/locale/locale_ja-JP.ini
+++ b/options/locale/locale_ja-JP.ini
@@ -1034,6 +1034,8 @@ fork_to_different_account=別のアカウントにフォークする
 fork_visibility_helper=フォークしたリポジトリの公開/非公開は変更できません。
 fork_branch=フォークにクローンされるブランチ
 all_branches=すべてのブランチ
+view_all_branches=すべてのブランチを表示
+view_all_tags=すべてのタグを表示
 fork_no_valid_owners=このリポジトリには有効なオーナーがいないため、フォークできません。
 fork.blocked_user=リポジトリのオーナーがあなたをブロックしているため、リポジトリをフォークできません。
 use_template=このテンプレートを使用
@@ -1108,6 +1110,7 @@ delete_preexisting_success=%s の未登録ファイルを削除しました
 blame_prior=この変更より前のBlameを表示
 blame.ignore_revs=<a href="%s">.git-blame-ignore-revs</a> で指定されたリビジョンは除外しています。 これを迂回して通常のBlame表示を見るには <a href="%s">ここ</a>をクリック。
 blame.ignore_revs.failed=<a href="%s">.git-blame-ignore-revs</a> によるリビジョンの無視は失敗しました。
+user_search_tooltip=最大30人までのユーザーを表示
 
 
 transfer.accept=移転を承認
@@ -1226,6 +1229,7 @@ create_new_repo_command=コマンドラインから新しいリポジトリを
 push_exist_repo=コマンドラインから既存のリポジトリをプッシュ
 empty_message=このリポジトリの中には何もありません。
 broken_message=このリポジトリの基礎となる Git のデータを読み取れません。このインスタンスの管理者に相談するか、このリポジトリを削除してください。
+no_branch=このリポジトリにはブランチがありません。
 
 code=コード
 code.desc=ソースコード、ファイル、コミット、ブランチにアクセス。
@@ -1523,6 +1527,8 @@ issues.filter_assignee=担当者
 issues.filter_assginee_no_select=すべての担当者
 issues.filter_assginee_no_assignee=担当者なし
 issues.filter_poster=作成者
+issues.filter_user_placeholder=ユーザーを検索
+issues.filter_user_no_select=すべてのユーザー
 issues.filter_type=タイプ
 issues.filter_type.all_issues=すべてのイシュー
 issues.filter_type.assigned_to_you=自分が担当
@@ -1674,16 +1680,16 @@ issues.timetracker_timer_manually_add=時間を追加
 
 issues.time_estimate_set=見積時間を設定
 issues.time_estimate_display=見積時間: %s
-issues.change_time_estimate_at=が見積時間を <b>%s</b> に変更 %s
+issues.change_time_estimate_at=が見積時間を <b>%[1]s</b> に変更 %[2]s
 issues.remove_time_estimate_at=が見積時間を削除 %s
 issues.time_estimate_invalid=見積時間のフォーマットが不正です
 issues.start_tracking_history=が作業を開始 %s
 issues.tracker_auto_close=タイマーは、このイシューがクローズされると自動的に終了します
 issues.tracking_already_started=`<a href="%s">別のイシュー</a>で既にタイムトラッキングを開始しています!`
-issues.stop_tracking_history=が <b>%s</b> の作業を終了 %s
+issues.stop_tracking_history=が <b>%[1]s</b> の作業を終了 %[2]s
 issues.cancel_tracking_history=`がタイムトラッキングを中止 %s`
 issues.del_time=このタイムログを削除
-issues.add_time_history=が作業時間 <b>%s</b> を追加 %s
+issues.add_time_history=が作業時間 <b>%[1]s</b> を追加 %[2]s
 issues.del_time_history=`が作業時間を削除 %s`
 issues.add_time_manually=時間の手入力
 issues.add_time_hours=時間
@@ -1938,6 +1944,8 @@ pulls.delete.title=このプルリクエストを削除しますか?
 pulls.delete.text=本当にこのプルリクエストを削除しますか? (これはすべてのコンテンツを完全に削除します。 保存しておきたい場合は、代わりにクローズすることを検討してください)
 
 pulls.recently_pushed_new_branches=%[2]s 、あなたはブランチ <strong>%[1]s</strong> にプッシュしました
+pulls.upstream_diverging_prompt_behind_1=このブランチは %[2]s よりも %[1]d コミット遅れています
+pulls.upstream_diverging_prompt_behind_n=このブランチは %[2]s よりも %[1]d コミット遅れています
 pulls.upstream_diverging_prompt_base_newer=ベースブランチ %s に新しい変更があります
 pulls.upstream_diverging_merge=フォークを同期
 
@@ -2621,6 +2629,7 @@ release.new_release=新しいリリース
 release.draft=下書き
 release.prerelease=プレリリース
 release.stable=安定版
+release.latest=最新
 release.compare=比較
 release.edit=編集
 release.ahead.commits=<strong>%d</strong>件のコミット
@@ -2849,6 +2858,7 @@ teams.invite.title=あなたは組織 <strong>%[2]s</strong> 内のチーム <st
 teams.invite.by=%s からの招待
 teams.invite.description=下のボタンをクリックしてチームに参加してください。
 
+view_as_role=表示: %s
 view_as_public_hint=READMEを公開ユーザーとして見ています。
 view_as_member_hint=READMEをこの組織のメンバーとして見ています。
 
@@ -3354,7 +3364,6 @@ monitor.execute_time=実行時間
 monitor.last_execution_result=結果
 monitor.process.cancel=処理をキャンセル
 monitor.process.cancel_desc=処理をキャンセルするとデータが失われる可能性があります
-monitor.process.cancel_notices=キャンセル: <strong>%s</strong>?
 monitor.process.children=子プロセス
 
 monitor.queues=キュー
@@ -3550,7 +3559,7 @@ conda.install=Conda を使用してパッケージをインストールするに
 container.details.type=イメージタイプ
 container.details.platform=プラットフォーム
 container.pull=コマンドラインでイメージを取得します:
-container.digest=ダイジェスト:
+container.digest=ダイジェスト
 container.multi_arch=OS / アーキテクチャ
 container.layers=イメージレイヤー
 container.labels=ラベル
diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini
index 8e1c4a651c..cc2dcd1180 100644
--- a/options/locale/locale_lv-LV.ini
+++ b/options/locale/locale_lv-LV.ini
@@ -3239,7 +3239,6 @@ conda.install=Lai instalētu Conda pakotni, izpildiet sekojošu komandu:
 container.details.type=Attēla formāts
 container.details.platform=Platforma
 container.pull=Atgādājiet šo attēlu no komandrindas:
-container.digest=Īssavilkums:
 container.multi_arch=OS / arhitektūra
 container.layers=Attēla slāņi
 container.labels=Iezīmes
diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini
index 4d049c83d1..4dfae86bb6 100644
--- a/options/locale/locale_pl-PL.ini
+++ b/options/locale/locale_pl-PL.ini
@@ -2310,7 +2310,6 @@ monitor.start=Czas rozpoczęcia
 monitor.execute_time=Czas wykonania
 monitor.process.cancel=Anuluj proces
 monitor.process.cancel_desc=Anulowanie procesu może spowodować utratę danych
-monitor.process.cancel_notices=Anuluj: <strong>%s</strong>?
 
 monitor.queues=Kolejki
 monitor.queue=Kolejka: %s
diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini
index f42de9287e..65d6b4569b 100644
--- a/options/locale/locale_pt-BR.ini
+++ b/options/locale/locale_pt-BR.ini
@@ -3180,7 +3180,6 @@ conda.install=Para instalar o pacote usando o Conda, execute o seguinte comando:
 container.details.type=Tipo de Imagem
 container.details.platform=Plataforma
 container.pull=Puxe a imagem pela linha de comando:
-container.digest=Digest:
 container.multi_arch=S.O. / Arquitetura
 container.layers=Camadas da Imagem
 container.labels=Rótulos
diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini
index 40f1a13d67..270728582a 100644
--- a/options/locale/locale_pt-PT.ini
+++ b/options/locale/locale_pt-PT.ini
@@ -1684,16 +1684,16 @@ issues.timetracker_timer_manually_add=Adicionar tempo
 
 issues.time_estimate_set=Definir tempo estimado
 issues.time_estimate_display=Estimativa: %s
-issues.change_time_estimate_at=alterou a estimativa de tempo para <b>%s</b> %s
+issues.change_time_estimate_at=alterou a estimativa de tempo para <b>%[1]s</b> %[2]s
 issues.remove_time_estimate_at=removeu a estimativa de tempo %s
 issues.time_estimate_invalid=O formato da estimativa de tempo é inválido
 issues.start_tracking_history=começou a trabalhar %s
 issues.tracker_auto_close=O cronómetro será parado automaticamente quando esta questão for fechada
 issues.tracking_already_started=`Você já iniciou a contagem de tempo <a href="%s">noutra questão</a>!`
-issues.stop_tracking_history=trabalhou durante <b>%s</b> %s
+issues.stop_tracking_history=trabalhou durante <b>%[1]s</b> %[2]s
 issues.cancel_tracking_history=`cancelou a contagem de tempo %s`
 issues.del_time=Eliminar este registo de tempo
-issues.add_time_history=adicionou <b>%s</b> de tempo gasto %s
+issues.add_time_history=adicionou <b>%[1]s</b> de tempo gasto %[2]s
 issues.del_time_history=`eliminou o tempo gasto nesta questão %s`
 issues.add_time_manually=Adicionar tempo manualmente
 issues.add_time_hours=Horas
@@ -2157,6 +2157,7 @@ settings.advanced_settings=Configurações avançadas
 settings.wiki_desc=Habilitar wiki do repositório
 settings.use_internal_wiki=Usar o wiki integrado
 settings.default_wiki_branch_name=Nome do ramo predefinido do wiki
+settings.default_permission_everyone_access=Permissão de acesso predefinida para todos os utilizadores registados:
 settings.failed_to_change_default_wiki_branch=Falhou ao mudar o nome do ramo predefinido do wiki.
 settings.use_external_wiki=Usar um wiki externo
 settings.external_wiki_url=URL do wiki externo
@@ -2711,6 +2712,8 @@ branch.create_branch_operation=Criar ramo
 branch.new_branch=Criar um novo ramo
 branch.new_branch_from=`Criar um novo ramo a partir do ramo "%s"`
 branch.renamed=O ramo %s foi renomeado para %s.
+branch.rename_default_or_protected_branch_error=Só os administradores é que podem renomear o ramo principal ou ramos protegidos.
+branch.rename_protected_branch_failed=Este ramo está protegido por regras de salvaguarda baseadas em padrões glob.
 
 tag.create_tag=Criar etiqueta %s
 tag.create_tag_operation=Criar etiqueta
@@ -3371,7 +3374,6 @@ monitor.execute_time=Tempo de execução
 monitor.last_execution_result=Resultado
 monitor.process.cancel=Cancelar processo
 monitor.process.cancel_desc=Cancelar um processo pode resultar na perda de dados
-monitor.process.cancel_notices=Cancelar: <strong>%s</strong>?
 monitor.process.children=Descendentes
 
 monitor.queues=Filas
@@ -3568,7 +3570,8 @@ conda.install=Para instalar o pacote usando o Conda, execute o seguinte comando:
 container.details.type=Tipo de imagem
 container.details.platform=Plataforma
 container.pull=Puxar a imagem usando a linha de comandos:
-container.digest=Resumo:
+container.images=Imagens
+container.digest=Resumo
 container.multi_arch=S.O. / Arquit.
 container.layers=Camadas de imagem
 container.labels=Rótulos
diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini
index 17ebb275a3..d3b673bd18 100644
--- a/options/locale/locale_ru-RU.ini
+++ b/options/locale/locale_ru-RU.ini
@@ -3176,7 +3176,6 @@ conda.install=Чтобы установить пакет с помощью Conda
 container.details.type=Тип образа
 container.details.platform=Платформа
 container.pull=Загрузите образ из командной строки:
-container.digest=Отпечаток:
 container.multi_arch=ОС / архитектура
 container.layers=Слои образа
 container.labels=Метки
diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini
index f64f4935a4..6d14f512ff 100644
--- a/options/locale/locale_tr-TR.ini
+++ b/options/locale/locale_tr-TR.ini
@@ -3430,7 +3430,6 @@ conda.install=Conda ile paket kurmak için aşağıdaki komutu çalıştırın:
 container.details.type=Görüntü Türü
 container.details.platform=Platform
 container.pull=Görüntüyü komut satırını kullanarak çekin:
-container.digest=Özet:
 container.multi_arch=İşletim Sistemi / Mimari
 container.layers=Görüntü Katmanları
 container.labels=Etiketler
diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini
index af590a68c1..92de8a1280 100644
--- a/options/locale/locale_zh-CN.ini
+++ b/options/locale/locale_zh-CN.ini
@@ -1678,16 +1678,13 @@ issues.timetracker_timer_manually_add=添加时间
 
 issues.time_estimate_set=设置预计时间
 issues.time_estimate_display=预计: %s
-issues.change_time_estimate_at=将预计时间修改为 <b>%s</b> %s
 issues.remove_time_estimate_at=删除预计时间 %s
 issues.time_estimate_invalid=预计时间格式无效
 issues.start_tracking_history=`开始工作 %s`
 issues.tracker_auto_close=当此工单关闭时,自动停止计时器
 issues.tracking_already_started=`你已经开始对 <a href="%s">另一个工单</a> 进行时间跟踪!`
-issues.stop_tracking_history=`停止工作 %s`
 issues.cancel_tracking_history=`取消时间跟踪 %s`
 issues.del_time=删除此时间跟踪日志
-issues.add_time_history=`添加计时 %s`
 issues.del_time_history=`已删除时间 %s`
 issues.add_time_manually=手动添加时间
 issues.add_time_hours=小时
@@ -3359,7 +3356,6 @@ monitor.execute_time=执行时长
 monitor.last_execution_result=结果
 monitor.process.cancel=中止进程
 monitor.process.cancel_desc=中止一个进程可能导致数据丢失
-monitor.process.cancel_notices=中止:<strong>%s</strong> ?
 monitor.process.children=子进程
 
 monitor.queues=队列
@@ -3555,7 +3551,6 @@ conda.install=要使用 Conda 安装软件包,请运行以下命令:
 container.details.type=镜像类型
 container.details.platform=平台
 container.pull=从命令行拉取镜像:
-container.digest=摘要:
 container.multi_arch=OS / Arch
 container.layers=镜像层
 container.labels=标签
diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini
index b6e9e58fda..d03d9cf1fa 100644
--- a/options/locale/locale_zh-TW.ini
+++ b/options/locale/locale_zh-TW.ini
@@ -1672,16 +1672,13 @@ issues.timetracker_timer_manually_add=手動新增時間
 
 issues.time_estimate_set=設定預估時間
 issues.time_estimate_display=預估時間:%s
-issues.change_time_estimate_at=將預估時間更改為 <b>%s</b> %s
 issues.remove_time_estimate_at=移除預估時間 %s
 issues.time_estimate_invalid=預估時間格式無效
 issues.start_tracking_history=`開始工作 %s`
 issues.tracker_auto_close=當這個問題被關閉時,自動停止計時器
 issues.tracking_already_started=`您已在<a href="%s">另一個問題</a>上開始時間追蹤!`
-issues.stop_tracking_history=`結束工作 %s`
 issues.cancel_tracking_history=`取消時間追蹤 %s`
 issues.del_time=刪除此時間記錄
-issues.add_time_history=`加入了花費時間 %s`
 issues.del_time_history=`刪除了花費時間 %s`
 issues.add_time_manually=手動新增時間
 issues.add_time_hours=小時
@@ -3350,7 +3347,6 @@ monitor.execute_time=已執行時間
 monitor.last_execution_result=結果
 monitor.process.cancel=結束處理程序
 monitor.process.cancel_desc=結束處理程序可能造成資料遺失
-monitor.process.cancel_notices=結束: <strong>%s</strong>?
 monitor.process.children=子程序
 
 monitor.queues=佇列
@@ -3546,7 +3542,6 @@ conda.install=執行下列命令以使用 Conda 安裝此套件:
 container.details.type=映像檔類型
 container.details.platform=平台
 container.pull=透過下列命令拉取映像檔:
-container.digest=摘要:
 container.multi_arch=作業系統 / 架構
 container.layers=映像檔 Layers
 container.labels=標籤
diff --git a/package-lock.json b/package-lock.json
index 89bffd0ff3..b993e40e73 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,18 +6,18 @@
     "": {
       "dependencies": {
         "@citation-js/core": "0.7.14",
-        "@citation-js/plugin-bibtex": "0.7.16",
+        "@citation-js/plugin-bibtex": "0.7.17",
         "@citation-js/plugin-csl": "0.7.14",
         "@citation-js/plugin-software-formats": "0.6.1",
         "@github/markdown-toolbar-element": "2.2.3",
-        "@github/relative-time-element": "4.4.4",
+        "@github/relative-time-element": "4.4.5",
         "@github/text-expander-element": "2.8.0",
         "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
         "@primer/octicons": "19.14.0",
         "@silverwind/vue3-calendar-heatmap": "2.0.6",
         "add-asset-webpack-plugin": "3.0.0",
         "ansi_up": "6.0.2",
-        "asciinema-player": "3.8.1",
+        "asciinema-player": "3.8.2",
         "chart.js": "4.4.7",
         "chartjs-adapter-dayjs-4": "1.0.4",
         "chartjs-plugin-zoom": "2.2.0",
@@ -29,11 +29,11 @@
         "easymde": "2.18.0",
         "esbuild-loader": "4.2.2",
         "escape-goat": "4.0.0",
-        "fast-glob": "3.3.2",
+        "fast-glob": "3.3.3",
         "htmx.org": "2.0.4",
-        "idiomorph": "0.3.0",
+        "idiomorph": "0.4.0",
         "jquery": "3.7.1",
-        "katex": "0.16.18",
+        "katex": "0.16.20",
         "license-checker-webpack-plugin": "0.2.1",
         "mermaid": "11.4.1",
         "mini-css-extract-plugin": "2.9.2",
@@ -42,7 +42,7 @@
         "monaco-editor-webpack-plugin": "7.1.0",
         "pdfobject": "2.3.0",
         "perfect-debounce": "1.0.0",
-        "postcss": "8.4.49",
+        "postcss": "8.5.1",
         "postcss-loader": "8.1.1",
         "postcss-nesting": "13.0.1",
         "sortablejs": "1.15.6",
@@ -53,7 +53,7 @@
         "tippy.js": "6.3.7",
         "toastify-js": "1.12.0",
         "tributejs": "5.1.3",
-        "typescript": "5.7.2",
+        "typescript": "5.7.3",
         "uint8-to-base64": "0.2.0",
         "vanilla-colorful": "0.7.2",
         "vue": "3.5.13",
@@ -61,14 +61,14 @@
         "vue-chartjs": "5.3.2",
         "vue-loader": "17.4.2",
         "webpack": "5.97.1",
-        "webpack-cli": "5.1.4",
+        "webpack-cli": "6.0.1",
         "wrap-ansi": "9.0.0"
       },
       "devDependencies": {
         "@eslint-community/eslint-plugin-eslint-comments": "4.4.1",
         "@playwright/test": "1.49.1",
         "@stoplight/spectral-cli": "6.14.2",
-        "@stylistic/eslint-plugin-js": "2.12.1",
+        "@stylistic/eslint-plugin-js": "2.13.0",
         "@stylistic/stylelint-plugin": "3.1.1",
         "@types/dropzone": "5.7.9",
         "@types/jquery": "3.5.32",
@@ -80,8 +80,8 @@
         "@types/throttle-debounce": "5.0.2",
         "@types/tinycolor2": "1.4.6",
         "@types/toastify-js": "1.12.3",
-        "@typescript-eslint/eslint-plugin": "8.18.1",
-        "@typescript-eslint/parser": "8.18.1",
+        "@typescript-eslint/eslint-plugin": "8.20.0",
+        "@typescript-eslint/parser": "8.20.0",
         "@vitejs/plugin-vue": "5.2.1",
         "eslint": "8.57.0",
         "eslint-import-resolver-typescript": "3.7.0",
@@ -99,16 +99,16 @@
         "eslint-plugin-vue": "9.32.0",
         "eslint-plugin-vue-scoped-css": "2.9.0",
         "eslint-plugin-wc": "2.2.0",
-        "happy-dom": "15.11.7",
+        "happy-dom": "16.6.0",
         "markdownlint-cli": "0.43.0",
         "nolyfill": "1.0.43",
-        "postcss-html": "1.7.0",
-        "stylelint": "16.12.0",
+        "postcss-html": "1.8.0",
+        "stylelint": "16.13.2",
         "stylelint-declaration-block-no-ignored-properties": "2.8.0",
-        "stylelint-declaration-strict-value": "1.10.6",
+        "stylelint-declaration-strict-value": "1.10.7",
         "stylelint-value-no-unknown-custom-properties": "6.0.1",
         "svgo": "3.3.2",
-        "type-fest": "4.30.2",
+        "type-fest": "4.32.0",
         "updates": "16.4.1",
         "vite-string-plugin": "1.3.4",
         "vitest": "2.1.8",
@@ -191,9 +191,9 @@
       }
     },
     "node_modules/@babel/compat-data": {
-      "version": "7.26.3",
-      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.3.tgz",
-      "integrity": "sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==",
+      "version": "7.26.5",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz",
+      "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -281,14 +281,14 @@
       }
     },
     "node_modules/@babel/generator": {
-      "version": "7.26.3",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz",
-      "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==",
+      "version": "7.26.5",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz",
+      "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@babel/parser": "^7.26.3",
-        "@babel/types": "^7.26.3",
+        "@babel/parser": "^7.26.5",
+        "@babel/types": "^7.26.5",
         "@jridgewell/gen-mapping": "^0.3.5",
         "@jridgewell/trace-mapping": "^0.3.25",
         "jsesc": "^3.0.2"
@@ -311,13 +311,13 @@
       }
     },
     "node_modules/@babel/helper-compilation-targets": {
-      "version": "7.25.9",
-      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz",
-      "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==",
+      "version": "7.26.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz",
+      "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@babel/compat-data": "^7.25.9",
+        "@babel/compat-data": "^7.26.5",
         "@babel/helper-validator-option": "^7.25.9",
         "browserslist": "^4.24.0",
         "lru-cache": "^5.1.1",
@@ -474,9 +474,9 @@
       }
     },
     "node_modules/@babel/helper-plugin-utils": {
-      "version": "7.25.9",
-      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz",
-      "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==",
+      "version": "7.26.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz",
+      "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -502,15 +502,15 @@
       }
     },
     "node_modules/@babel/helper-replace-supers": {
-      "version": "7.25.9",
-      "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz",
-      "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==",
+      "version": "7.26.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz",
+      "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "@babel/helper-member-expression-to-functions": "^7.25.9",
         "@babel/helper-optimise-call-expression": "^7.25.9",
-        "@babel/traverse": "^7.25.9"
+        "@babel/traverse": "^7.26.5"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -591,12 +591,12 @@
       }
     },
     "node_modules/@babel/parser": {
-      "version": "7.26.3",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz",
-      "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==",
+      "version": "7.26.5",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.5.tgz",
+      "integrity": "sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==",
       "license": "MIT",
       "dependencies": {
-        "@babel/types": "^7.26.3"
+        "@babel/types": "^7.26.5"
       },
       "bin": {
         "parser": "bin/babel-parser.js"
@@ -870,13 +870,13 @@
       }
     },
     "node_modules/@babel/plugin-transform-block-scoped-functions": {
-      "version": "7.25.9",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz",
-      "integrity": "sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==",
+      "version": "7.26.5",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz",
+      "integrity": "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.25.9"
+        "@babel/helper-plugin-utils": "^7.26.5"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -1098,14 +1098,14 @@
       }
     },
     "node_modules/@babel/plugin-transform-flow-strip-types": {
-      "version": "7.25.9",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.25.9.tgz",
-      "integrity": "sha512-/VVukELzPDdci7UUsWQaSkhgnjIWXnIyRpM02ldxaVoFK96c41So8JcKT3m0gYjyv7j5FNPGS5vfELrWalkbDA==",
+      "version": "7.26.5",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.26.5.tgz",
+      "integrity": "sha512-eGK26RsbIkYUns3Y8qKl362juDDYK+wEdPGHGrhzUl6CewZFo55VZ7hg+CyMFU4dd5QQakBN86nBMpRsFpRvbQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.25.9",
-        "@babel/plugin-syntax-flow": "^7.25.9"
+        "@babel/helper-plugin-utils": "^7.26.5",
+        "@babel/plugin-syntax-flow": "^7.26.0"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -1317,13 +1317,13 @@
       }
     },
     "node_modules/@babel/plugin-transform-nullish-coalescing-operator": {
-      "version": "7.25.9",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.9.tgz",
-      "integrity": "sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog==",
+      "version": "7.26.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz",
+      "integrity": "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.25.9"
+        "@babel/helper-plugin-utils": "^7.26.5"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -1926,17 +1926,17 @@
       }
     },
     "node_modules/@babel/traverse": {
-      "version": "7.26.4",
-      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz",
-      "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==",
+      "version": "7.26.5",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.5.tgz",
+      "integrity": "sha512-rkOSPOw+AXbgtwUga3U4u8RpoK9FEFWBNAlTpcnkLFjL5CT+oyHNuUUC/xx6XefEJ16r38r8Bc/lfp6rYuHeJQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "@babel/code-frame": "^7.26.2",
-        "@babel/generator": "^7.26.3",
-        "@babel/parser": "^7.26.3",
+        "@babel/generator": "^7.26.5",
+        "@babel/parser": "^7.26.5",
         "@babel/template": "^7.25.9",
-        "@babel/types": "^7.26.3",
+        "@babel/types": "^7.26.5",
         "debug": "^4.3.1",
         "globals": "^11.1.0"
       },
@@ -1955,9 +1955,9 @@
       }
     },
     "node_modules/@babel/types": {
-      "version": "7.26.3",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz",
-      "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==",
+      "version": "7.26.5",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.5.tgz",
+      "integrity": "sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==",
       "license": "MIT",
       "dependencies": {
         "@babel/helper-string-parser": "^7.25.9",
@@ -1968,9 +1968,9 @@
       }
     },
     "node_modules/@braintree/sanitize-url": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.0.tgz",
-      "integrity": "sha512-o+UlMLt49RvtCASlOMW0AkHnabN9wR9rwCCherxO0yG4Npy34GkvrAqdXQvrhNs+jh+gkK8gB8Lf05qL/O7KWg==",
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz",
+      "integrity": "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==",
       "license": "MIT"
     },
     "node_modules/@chevrotain/cst-dts-gen": {
@@ -2046,9 +2046,9 @@
       }
     },
     "node_modules/@citation-js/plugin-bibtex": {
-      "version": "0.7.16",
-      "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibtex/-/plugin-bibtex-0.7.16.tgz",
-      "integrity": "sha512-Udeli19VAoFjOw0H1bB1KgmekRoW6XP5cdR3OQF5c2Mt1tZatXWcSQVdq+FeLKzodRocZXG5NFRvjyUZjVbV6A==",
+      "version": "0.7.17",
+      "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibtex/-/plugin-bibtex-0.7.17.tgz",
+      "integrity": "sha512-pyMW6UR6iMPCk1mVwagNHabprajOCQO+TibxKI6ymdv5VOX3zoqeQF0utwjFnViquL/BZfM5SGUZCQdu+ZZYag==",
       "license": "MIT",
       "dependencies": {
         "@citation-js/date": "^0.5.0",
@@ -2226,12 +2226,12 @@
       }
     },
     "node_modules/@discoveryjs/json-ext": {
-      "version": "0.5.7",
-      "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz",
-      "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==",
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz",
+      "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==",
       "license": "MIT",
       "engines": {
-        "node": ">=10.0.0"
+        "node": ">=14.17.0"
       }
     },
     "node_modules/@dual-bundle/import-meta-resolve": {
@@ -2808,9 +2808,9 @@
       "license": "MIT"
     },
     "node_modules/@github/relative-time-element": {
-      "version": "4.4.4",
-      "resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.4.4.tgz",
-      "integrity": "sha512-Oi8uOL8O+ZWLD7dHRWCkm2cudcTYtB3VyOYf9BtzCgDGm+OKomyOREtItNMtWl1dxvec62BTKErq36uy+RYxQg==",
+      "version": "4.4.5",
+      "resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.4.5.tgz",
+      "integrity": "sha512-9ejPtayBDIJfEU8x1fg/w2o5mahHkkp1SC6uObDtoKs4Gn+2a1vNK8XIiNDD8rMeEfpvDjydgSZZ+uk+7N0VsQ==",
       "license": "MIT"
     },
     "node_modules/@github/text-expander-element": {
@@ -3106,6 +3106,41 @@
         "jsep": "^0.4.0||^1.0.0"
       }
     },
+    "node_modules/@keyv/serialize": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.0.2.tgz",
+      "integrity": "sha512-+E/LyaAeuABniD/RvUezWVXKpeuvwLEA9//nE9952zBaOdBd2mQ3pPoM8cUe2X6IcMByfuSLzmYqnYshG60+HQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "buffer": "^6.0.3"
+      }
+    },
+    "node_modules/@keyv/serialize/node_modules/buffer": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+      "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "base64-js": "^1.3.1",
+        "ieee754": "^1.2.1"
+      }
+    },
     "node_modules/@kurkle/color": {
       "version": "0.3.4",
       "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
@@ -3364,9 +3399,9 @@
       "license": "MIT"
     },
     "node_modules/@rollup/rollup-android-arm-eabi": {
-      "version": "4.28.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz",
-      "integrity": "sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==",
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.30.1.tgz",
+      "integrity": "sha512-pSWY+EVt3rJ9fQ3IqlrEUtXh3cGqGtPDH1FQlNZehO2yYxCHEX1SPsz1M//NXwYfbTlcKr9WObLnJX9FsS9K1Q==",
       "cpu": [
         "arm"
       ],
@@ -3378,9 +3413,9 @@
       ]
     },
     "node_modules/@rollup/rollup-android-arm64": {
-      "version": "4.28.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz",
-      "integrity": "sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==",
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.30.1.tgz",
+      "integrity": "sha512-/NA2qXxE3D/BRjOJM8wQblmArQq1YoBVJjrjoTSBS09jgUisq7bqxNHJ8kjCHeV21W/9WDGwJEWSN0KQ2mtD/w==",
       "cpu": [
         "arm64"
       ],
@@ -3392,9 +3427,9 @@
       ]
     },
     "node_modules/@rollup/rollup-darwin-arm64": {
-      "version": "4.28.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz",
-      "integrity": "sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==",
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.30.1.tgz",
+      "integrity": "sha512-r7FQIXD7gB0WJ5mokTUgUWPl0eYIH0wnxqeSAhuIwvnnpjdVB8cRRClyKLQr7lgzjctkbp5KmswWszlwYln03Q==",
       "cpu": [
         "arm64"
       ],
@@ -3406,9 +3441,9 @@
       ]
     },
     "node_modules/@rollup/rollup-darwin-x64": {
-      "version": "4.28.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz",
-      "integrity": "sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==",
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.30.1.tgz",
+      "integrity": "sha512-x78BavIwSH6sqfP2xeI1hd1GpHL8J4W2BXcVM/5KYKoAD3nNsfitQhvWSw+TFtQTLZ9OmlF+FEInEHyubut2OA==",
       "cpu": [
         "x64"
       ],
@@ -3420,9 +3455,9 @@
       ]
     },
     "node_modules/@rollup/rollup-freebsd-arm64": {
-      "version": "4.28.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz",
-      "integrity": "sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==",
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.30.1.tgz",
+      "integrity": "sha512-HYTlUAjbO1z8ywxsDFWADfTRfTIIy/oUlfIDmlHYmjUP2QRDTzBuWXc9O4CXM+bo9qfiCclmHk1x4ogBjOUpUQ==",
       "cpu": [
         "arm64"
       ],
@@ -3434,9 +3469,9 @@
       ]
     },
     "node_modules/@rollup/rollup-freebsd-x64": {
-      "version": "4.28.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz",
-      "integrity": "sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==",
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.30.1.tgz",
+      "integrity": "sha512-1MEdGqogQLccphhX5myCJqeGNYTNcmTyaic9S7CG3JhwuIByJ7J05vGbZxsizQthP1xpVx7kd3o31eOogfEirw==",
       "cpu": [
         "x64"
       ],
@@ -3448,9 +3483,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
-      "version": "4.28.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz",
-      "integrity": "sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==",
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.30.1.tgz",
+      "integrity": "sha512-PaMRNBSqCx7K3Wc9QZkFx5+CX27WFpAMxJNiYGAXfmMIKC7jstlr32UhTgK6T07OtqR+wYlWm9IxzennjnvdJg==",
       "cpu": [
         "arm"
       ],
@@ -3462,9 +3497,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm-musleabihf": {
-      "version": "4.28.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz",
-      "integrity": "sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==",
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.30.1.tgz",
+      "integrity": "sha512-B8Rcyj9AV7ZlEFqvB5BubG5iO6ANDsRKlhIxySXcF1axXYUyqwBok+XZPgIYGBgs7LDXfWfifxhw0Ik57T0Yug==",
       "cpu": [
         "arm"
       ],
@@ -3476,9 +3511,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-gnu": {
-      "version": "4.28.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz",
-      "integrity": "sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==",
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.30.1.tgz",
+      "integrity": "sha512-hqVyueGxAj3cBKrAI4aFHLV+h0Lv5VgWZs9CUGqr1z0fZtlADVV1YPOij6AhcK5An33EXaxnDLmJdQikcn5NEw==",
       "cpu": [
         "arm64"
       ],
@@ -3490,9 +3525,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-musl": {
-      "version": "4.28.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz",
-      "integrity": "sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==",
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.30.1.tgz",
+      "integrity": "sha512-i4Ab2vnvS1AE1PyOIGp2kXni69gU2DAUVt6FSXeIqUCPIR3ZlheMW3oP2JkukDfu3PsexYRbOiJrY+yVNSk9oA==",
       "cpu": [
         "arm64"
       ],
@@ -3504,9 +3539,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
-      "version": "4.28.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz",
-      "integrity": "sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==",
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.30.1.tgz",
+      "integrity": "sha512-fARcF5g296snX0oLGkVxPmysetwUk2zmHcca+e9ObOovBR++9ZPOhqFUM61UUZ2EYpXVPN1redgqVoBB34nTpQ==",
       "cpu": [
         "loong64"
       ],
@@ -3518,9 +3553,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
-      "version": "4.28.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz",
-      "integrity": "sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==",
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.30.1.tgz",
+      "integrity": "sha512-GLrZraoO3wVT4uFXh67ElpwQY0DIygxdv0BNW9Hkm3X34wu+BkqrDrkcsIapAY+N2ATEbvak0XQ9gxZtCIA5Rw==",
       "cpu": [
         "ppc64"
       ],
@@ -3532,9 +3567,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-riscv64-gnu": {
-      "version": "4.28.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz",
-      "integrity": "sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==",
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.30.1.tgz",
+      "integrity": "sha512-0WKLaAUUHKBtll0wvOmh6yh3S0wSU9+yas923JIChfxOaaBarmb/lBKPF0w/+jTVozFnOXJeRGZ8NvOxvk/jcw==",
       "cpu": [
         "riscv64"
       ],
@@ -3546,9 +3581,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-s390x-gnu": {
-      "version": "4.28.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz",
-      "integrity": "sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==",
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.30.1.tgz",
+      "integrity": "sha512-GWFs97Ruxo5Bt+cvVTQkOJ6TIx0xJDD/bMAOXWJg8TCSTEK8RnFeOeiFTxKniTc4vMIaWvCplMAFBt9miGxgkA==",
       "cpu": [
         "s390x"
       ],
@@ -3560,9 +3595,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-x64-gnu": {
-      "version": "4.28.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz",
-      "integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==",
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.30.1.tgz",
+      "integrity": "sha512-UtgGb7QGgXDIO+tqqJ5oZRGHsDLO8SlpE4MhqpY9Llpzi5rJMvrK6ZGhsRCST2abZdBqIBeXW6WPD5fGK5SDwg==",
       "cpu": [
         "x64"
       ],
@@ -3574,9 +3609,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-x64-musl": {
-      "version": "4.28.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz",
-      "integrity": "sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==",
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.30.1.tgz",
+      "integrity": "sha512-V9U8Ey2UqmQsBT+xTOeMzPzwDzyXmnAoO4edZhL7INkwQcaW1Ckv3WJX3qrrp/VHaDkEWIBWhRwP47r8cdrOow==",
       "cpu": [
         "x64"
       ],
@@ -3588,9 +3623,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-arm64-msvc": {
-      "version": "4.28.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz",
-      "integrity": "sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==",
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.30.1.tgz",
+      "integrity": "sha512-WabtHWiPaFF47W3PkHnjbmWawnX/aE57K47ZDT1BXTS5GgrBUEpvOzq0FI0V/UYzQJgdb8XlhVNH8/fwV8xDjw==",
       "cpu": [
         "arm64"
       ],
@@ -3602,9 +3637,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-ia32-msvc": {
-      "version": "4.28.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz",
-      "integrity": "sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==",
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.30.1.tgz",
+      "integrity": "sha512-pxHAU+Zv39hLUTdQQHUVHf4P+0C47y/ZloorHpzs2SXMRqeAWmGghzAhfOlzFHHwjvgokdFAhC4V+6kC1lRRfw==",
       "cpu": [
         "ia32"
       ],
@@ -3616,9 +3651,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-x64-msvc": {
-      "version": "4.28.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz",
-      "integrity": "sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==",
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.30.1.tgz",
+      "integrity": "sha512-D6qjsXGcvhTjv0kI4fU8tUuBDF/Ueee4SVX79VfNDXZa64TfCW1Slkb6Z7O1p7vflqZjcmOVdZlqf8gvJxc6og==",
       "cpu": [
         "x64"
       ],
@@ -4175,9 +4210,9 @@
       }
     },
     "node_modules/@stylistic/eslint-plugin-js": {
-      "version": "2.12.1",
-      "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.12.1.tgz",
-      "integrity": "sha512-5ybogtEgWIGCR6dMnaabztbWyVdAPDsf/5XOk6jBonWug875Q9/a6gm9QxnU3rhdyDEnckWKX7dduwYJMOWrVA==",
+      "version": "2.13.0",
+      "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.13.0.tgz",
+      "integrity": "sha512-GPPDK4+fcbsQD58a3abbng2Dx+jBoxM5cnYjBM4T24WFZRZdlNSKvR19TxP8CPevzMOodQ9QVzNeqWvMXzfJRA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -4447,9 +4482,9 @@
       "license": "MIT"
     },
     "node_modules/@types/d3-shape": {
-      "version": "3.1.6",
-      "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz",
-      "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==",
+      "version": "3.1.7",
+      "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
+      "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
       "license": "MIT",
       "dependencies": {
         "@types/d3-path": "*"
@@ -4611,9 +4646,9 @@
       "license": "MIT"
     },
     "node_modules/@types/node": {
-      "version": "22.10.2",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
-      "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==",
+      "version": "22.10.6",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.6.tgz",
+      "integrity": "sha512-qNiuwC4ZDAUNcY47xgaSuS92cjf8JbSUoaKS77bmLG1rU7MlATVSiw/IlrjtIyyskXBZ8KkNfjK/P5na7rgXbQ==",
       "license": "MIT",
       "dependencies": {
         "undici-types": "~6.20.0"
@@ -4767,21 +4802,21 @@
       }
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "8.18.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.1.tgz",
-      "integrity": "sha512-Ncvsq5CT3Gvh+uJG0Lwlho6suwDfUXH0HztslDf5I+F2wAFAZMRwYLEorumpKLzmO2suAXZ/td1tBg4NZIi9CQ==",
+      "version": "8.20.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.20.0.tgz",
+      "integrity": "sha512-naduuphVw5StFfqp4Gq4WhIBE2gN1GEmMUExpJYknZJdRnc+2gDzB8Z3+5+/Kv33hPQRDGzQO/0opHE72lZZ6A==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "@eslint-community/regexpp": "^4.10.0",
-        "@typescript-eslint/scope-manager": "8.18.1",
-        "@typescript-eslint/type-utils": "8.18.1",
-        "@typescript-eslint/utils": "8.18.1",
-        "@typescript-eslint/visitor-keys": "8.18.1",
+        "@typescript-eslint/scope-manager": "8.20.0",
+        "@typescript-eslint/type-utils": "8.20.0",
+        "@typescript-eslint/utils": "8.20.0",
+        "@typescript-eslint/visitor-keys": "8.20.0",
         "graphemer": "^1.4.0",
         "ignore": "^5.3.1",
         "natural-compare": "^1.4.0",
-        "ts-api-utils": "^1.3.0"
+        "ts-api-utils": "^2.0.0"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -4797,16 +4832,16 @@
       }
     },
     "node_modules/@typescript-eslint/parser": {
-      "version": "8.18.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.1.tgz",
-      "integrity": "sha512-rBnTWHCdbYM2lh7hjyXqxk70wvon3p2FyaniZuey5TrcGBpfhVp0OxOa6gxr9Q9YhZFKyfbEnxc24ZnVbbUkCA==",
+      "version": "8.20.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.20.0.tgz",
+      "integrity": "sha512-gKXG7A5HMyjDIedBi6bUrDcun8GIjnI8qOwVLiY3rx6T/sHP/19XLJOnIq/FgQvWLHja5JN/LSE7eklNBr612g==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/scope-manager": "8.18.1",
-        "@typescript-eslint/types": "8.18.1",
-        "@typescript-eslint/typescript-estree": "8.18.1",
-        "@typescript-eslint/visitor-keys": "8.18.1",
+        "@typescript-eslint/scope-manager": "8.20.0",
+        "@typescript-eslint/types": "8.20.0",
+        "@typescript-eslint/typescript-estree": "8.20.0",
+        "@typescript-eslint/visitor-keys": "8.20.0",
         "debug": "^4.3.4"
       },
       "engines": {
@@ -4822,14 +4857,14 @@
       }
     },
     "node_modules/@typescript-eslint/scope-manager": {
-      "version": "8.18.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.1.tgz",
-      "integrity": "sha512-HxfHo2b090M5s2+/9Z3gkBhI6xBH8OJCFjH9MhQ+nnoZqxU3wNxkLT+VWXWSFWc3UF3Z+CfPAyqdCTdoXtDPCQ==",
+      "version": "8.20.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.20.0.tgz",
+      "integrity": "sha512-J7+VkpeGzhOt3FeG1+SzhiMj9NzGD/M6KoGn9f4dbz3YzK9hvbhVTmLj/HiTp9DazIzJ8B4XcM80LrR9Dm1rJw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/types": "8.18.1",
-        "@typescript-eslint/visitor-keys": "8.18.1"
+        "@typescript-eslint/types": "8.20.0",
+        "@typescript-eslint/visitor-keys": "8.20.0"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -4840,16 +4875,16 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils": {
-      "version": "8.18.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.1.tgz",
-      "integrity": "sha512-jAhTdK/Qx2NJPNOTxXpMwlOiSymtR2j283TtPqXkKBdH8OAMmhiUfP0kJjc/qSE51Xrq02Gj9NY7MwK+UxVwHQ==",
+      "version": "8.20.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.20.0.tgz",
+      "integrity": "sha512-bPC+j71GGvA7rVNAHAtOjbVXbLN5PkwqMvy1cwGeaxUoRQXVuKCebRoLzm+IPW/NtFFpstn1ummSIasD5t60GA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/typescript-estree": "8.18.1",
-        "@typescript-eslint/utils": "8.18.1",
+        "@typescript-eslint/typescript-estree": "8.20.0",
+        "@typescript-eslint/utils": "8.20.0",
         "debug": "^4.3.4",
-        "ts-api-utils": "^1.3.0"
+        "ts-api-utils": "^2.0.0"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -4864,9 +4899,9 @@
       }
     },
     "node_modules/@typescript-eslint/types": {
-      "version": "8.18.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.1.tgz",
-      "integrity": "sha512-7uoAUsCj66qdNQNpH2G8MyTFlgerum8ubf21s3TSM3XmKXuIn+H2Sifh/ES2nPOPiYSRJWAk0fDkW0APBWcpfw==",
+      "version": "8.20.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.20.0.tgz",
+      "integrity": "sha512-cqaMiY72CkP+2xZRrFt3ExRBu0WmVitN/rYPZErA80mHjHx/Svgp8yfbzkJmDoQ/whcytOPO9/IZXnOc+wigRA==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -4878,20 +4913,20 @@
       }
     },
     "node_modules/@typescript-eslint/typescript-estree": {
-      "version": "8.18.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.1.tgz",
-      "integrity": "sha512-z8U21WI5txzl2XYOW7i9hJhxoKKNG1kcU4RzyNvKrdZDmbjkmLBo8bgeiOJmA06kizLI76/CCBAAGlTlEeUfyg==",
+      "version": "8.20.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.20.0.tgz",
+      "integrity": "sha512-Y7ncuy78bJqHI35NwzWol8E0X7XkRVS4K4P4TCyzWkOJih5NDvtoRDW4Ba9YJJoB2igm9yXDdYI/+fkiiAxPzA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/types": "8.18.1",
-        "@typescript-eslint/visitor-keys": "8.18.1",
+        "@typescript-eslint/types": "8.20.0",
+        "@typescript-eslint/visitor-keys": "8.20.0",
         "debug": "^4.3.4",
         "fast-glob": "^3.3.2",
         "is-glob": "^4.0.3",
         "minimatch": "^9.0.4",
         "semver": "^7.6.0",
-        "ts-api-utils": "^1.3.0"
+        "ts-api-utils": "^2.0.0"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -4921,16 +4956,16 @@
       }
     },
     "node_modules/@typescript-eslint/utils": {
-      "version": "8.18.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.1.tgz",
-      "integrity": "sha512-8vikiIj2ebrC4WRdcAdDcmnu9Q/MXXwg+STf40BVfT8exDqBCUPdypvzcUPxEqRGKg9ALagZ0UWcYCtn+4W2iQ==",
+      "version": "8.20.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.20.0.tgz",
+      "integrity": "sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
-        "@typescript-eslint/scope-manager": "8.18.1",
-        "@typescript-eslint/types": "8.18.1",
-        "@typescript-eslint/typescript-estree": "8.18.1"
+        "@typescript-eslint/scope-manager": "8.20.0",
+        "@typescript-eslint/types": "8.20.0",
+        "@typescript-eslint/typescript-estree": "8.20.0"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -4945,13 +4980,13 @@
       }
     },
     "node_modules/@typescript-eslint/visitor-keys": {
-      "version": "8.18.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.1.tgz",
-      "integrity": "sha512-Vj0WLm5/ZsD013YeUKn+K0y8p1M0jPpxOkKdbD1wB0ns53a5piVY02zjf072TblEweAbcYiFiPoSMF3kp+VhhQ==",
+      "version": "8.20.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.20.0.tgz",
+      "integrity": "sha512-v/BpkeeYAsPkKCkR8BDwcno0llhzWVqPOamQrAEMdpZav2Y9OVjd9dwJyBLJWwf335B5DmlifECIkZRJCaGaHA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/types": "8.18.1",
+        "@typescript-eslint/types": "8.20.0",
         "eslint-visitor-keys": "^4.2.0"
       },
       "engines": {
@@ -5080,6 +5115,13 @@
         "url": "https://opencollective.com/vitest"
       }
     },
+    "node_modules/@vitest/runner/node_modules/pathe": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
+      "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@vitest/snapshot": {
       "version": "2.1.8",
       "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.8.tgz",
@@ -5105,6 +5147,13 @@
         "@jridgewell/sourcemap-codec": "^1.5.0"
       }
     },
+    "node_modules/@vitest/snapshot/node_modules/pathe": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
+      "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@vitest/spy": {
       "version": "2.1.8",
       "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.8.tgz",
@@ -5470,42 +5519,42 @@
       }
     },
     "node_modules/@webpack-cli/configtest": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz",
-      "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==",
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-3.0.1.tgz",
+      "integrity": "sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==",
       "license": "MIT",
       "engines": {
-        "node": ">=14.15.0"
+        "node": ">=18.12.0"
       },
       "peerDependencies": {
-        "webpack": "5.x.x",
-        "webpack-cli": "5.x.x"
+        "webpack": "^5.82.0",
+        "webpack-cli": "6.x.x"
       }
     },
     "node_modules/@webpack-cli/info": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz",
-      "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==",
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-3.0.1.tgz",
+      "integrity": "sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==",
       "license": "MIT",
       "engines": {
-        "node": ">=14.15.0"
+        "node": ">=18.12.0"
       },
       "peerDependencies": {
-        "webpack": "5.x.x",
-        "webpack-cli": "5.x.x"
+        "webpack": "^5.82.0",
+        "webpack-cli": "6.x.x"
       }
     },
     "node_modules/@webpack-cli/serve": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz",
-      "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==",
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-3.0.1.tgz",
+      "integrity": "sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==",
       "license": "MIT",
       "engines": {
-        "node": ">=14.15.0"
+        "node": ">=18.12.0"
       },
       "peerDependencies": {
-        "webpack": "5.x.x",
-        "webpack-cli": "5.x.x"
+        "webpack": "^5.82.0",
+        "webpack-cli": "6.x.x"
       },
       "peerDependenciesMeta": {
         "webpack-dev-server": {
@@ -5819,9 +5868,9 @@
       }
     },
     "node_modules/asciinema-player": {
-      "version": "3.8.1",
-      "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.8.1.tgz",
-      "integrity": "sha512-NkpbFg81Y6iJFpDRndakLCQ0G26XSpvuT3vJTFjMRgHb26lqHgRNY9gun54e5MehZ4fEDNYkMZv+z6MfZ8c2aA==",
+      "version": "3.8.2",
+      "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.8.2.tgz",
+      "integrity": "sha512-Lgcnj9u/H6sRpGRX1my7Azcay6llLmB/GVkCGcDbPwdTVTisS1ir8SQ9jRWRvjlLUjpSJkN0euruvy3sLRM8tw==",
       "license": "Apache-2.0",
       "dependencies": {
         "@babel/runtime": "^7.21.0",
@@ -6039,9 +6088,9 @@
       }
     },
     "node_modules/browserslist": {
-      "version": "4.24.3",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz",
-      "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==",
+      "version": "4.24.4",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
+      "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
       "funding": [
         {
           "type": "opencollective",
@@ -6140,6 +6189,27 @@
         "node": ">=8"
       }
     },
+    "node_modules/cacheable": {
+      "version": "1.8.7",
+      "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.8.7.tgz",
+      "integrity": "sha512-AbfG7dAuYNjYxFUtL1lAqmlWdxczCJ47w7cFjhGcnGnUdwSo6VgmSojfoW3tUI12HUkgTJ5kqj78yyq6TsFtlg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "hookified": "^1.6.0",
+        "keyv": "^5.2.3"
+      }
+    },
+    "node_modules/cacheable/node_modules/keyv": {
+      "version": "5.2.3",
+      "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.2.3.tgz",
+      "integrity": "sha512-AGKecUfzrowabUv0bH1RIR5Vf7w+l4S3xtQAypKaUpTdIR1EbrAcTxHCrpo9Q+IWeUlFE2palRtgIQcgm+PQJw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@keyv/serialize": "^1.0.2"
+      }
+    },
     "node_modules/callsites": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -6159,9 +6229,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001690",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz",
-      "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==",
+      "version": "1.0.30001692",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz",
+      "integrity": "sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==",
       "funding": [
         {
           "type": "opencollective",
@@ -6529,13 +6599,13 @@
       "license": "MIT"
     },
     "node_modules/core-js-compat": {
-      "version": "3.39.0",
-      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz",
-      "integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==",
+      "version": "3.40.0",
+      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz",
+      "integrity": "sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "browserslist": "^4.24.2"
+        "browserslist": "^4.24.3"
       },
       "funding": {
         "type": "opencollective",
@@ -6753,9 +6823,9 @@
       "license": "MIT"
     },
     "node_modules/cytoscape": {
-      "version": "3.30.4",
-      "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.30.4.tgz",
-      "integrity": "sha512-OxtlZwQl1WbwMmLiyPSEBuzeTIQnwZhJYYWFzZ2PhEHVFwpeaqNIkUzSiso00D98qk60l8Gwon2RP304d3BJ1A==",
+      "version": "3.31.0",
+      "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.31.0.tgz",
+      "integrity": "sha512-zDGn1K/tfZwEnoGOcHc0H4XazqAAXAuDpcYw9mUnUjATjqljyCNGJv8uEvbvxGaGHaVshxMecyl6oc6uKzRfbw==",
       "license": "MIT",
       "engines": {
         "node": ">=0.10"
@@ -7461,9 +7531,9 @@
       }
     },
     "node_modules/domutils": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
-      "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
+      "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
       "dev": true,
       "license": "BSD-2-Clause",
       "dependencies": {
@@ -7505,9 +7575,9 @@
       }
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.5.74",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.74.tgz",
-      "integrity": "sha512-ck3//9RC+6oss/1Bh9tiAVFy5vfSKbRHAFh7Z3/eTRkEqJeWgymloShB17Vg3Z4nmDNp35vAd1BZ6CMW4Wt6Iw==",
+      "version": "1.5.82",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.82.tgz",
+      "integrity": "sha512-Zq16uk1hfQhyGx5GpwPAYDwddJuSGhtRhgOA2mCxANYaDT79nAeGnaXogMGng4KqLaJUVnOnuL0+TDop9nLOiA==",
       "license": "ISC"
     },
     "node_modules/emoji-regex": {
@@ -7526,9 +7596,9 @@
       }
     },
     "node_modules/enhanced-resolve": {
-      "version": "5.17.1",
-      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
-      "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==",
+      "version": "5.18.0",
+      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz",
+      "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==",
       "license": "MIT",
       "dependencies": {
         "graceful-fs": "^4.2.4",
@@ -7595,9 +7665,9 @@
       }
     },
     "node_modules/es-module-lexer": {
-      "version": "1.5.4",
-      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz",
-      "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz",
+      "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==",
       "license": "MIT"
     },
     "node_modules/esbuild": {
@@ -7764,13 +7834,13 @@
       }
     },
     "node_modules/eslint-config-prettier": {
-      "version": "9.1.0",
-      "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
-      "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
+      "version": "10.0.1",
+      "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.1.tgz",
+      "integrity": "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw==",
       "dev": true,
       "license": "MIT",
       "bin": {
-        "eslint-config-prettier": "bin/cli.js"
+        "eslint-config-prettier": "build/bin/cli.js"
       },
       "peerDependencies": {
         "eslint": ">=7.0.0"
@@ -8221,9 +8291,9 @@
       }
     },
     "node_modules/eslint-plugin-prettier": {
-      "version": "5.2.1",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz",
-      "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==",
+      "version": "5.2.2",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.2.tgz",
+      "integrity": "sha512-1yI3/hf35wmlq66C8yOyrujQnel+v5l1Vop5Cl2I6ylyNTT1JbuUUnV3/41PzwTzcyDp/oF0jWE3HXvcH5AQOQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -8526,6 +8596,19 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
+    "node_modules/eslint-plugin-vitest/node_modules/ts-api-utils": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
+      "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=16"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.2.0"
+      }
+    },
     "node_modules/eslint-plugin-vue": {
       "version": "9.32.0",
       "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.32.0.tgz",
@@ -8820,16 +8903,16 @@
       "license": "Apache-2.0"
     },
     "node_modules/fast-glob": {
-      "version": "3.3.2",
-      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
-      "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+      "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
       "license": "MIT",
       "dependencies": {
         "@nodelib/fs.stat": "^2.0.2",
         "@nodelib/fs.walk": "^1.2.3",
         "glob-parent": "^5.1.2",
         "merge2": "^1.3.0",
-        "micromatch": "^4.0.4"
+        "micromatch": "^4.0.8"
       },
       "engines": {
         "node": ">=8.6.0"
@@ -8868,9 +8951,19 @@
       "license": "MIT"
     },
     "node_modules/fast-uri": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz",
-      "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==",
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.5.tgz",
+      "integrity": "sha512-5JnBCWpFlMo0a3ciDy/JckMzzv1U9coZrIhedq+HXxxUfDTAiS0LA8OKVao4G9BxmCVck/jtA5r3KAtRWEyD8Q==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fastify"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fastify"
+        }
+      ],
       "license": "BSD-3-Clause"
     },
     "node_modules/fastest-levenshtein": {
@@ -8883,9 +8976,9 @@
       }
     },
     "node_modules/fastq": {
-      "version": "1.17.1",
-      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
-      "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
+      "version": "1.18.0",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz",
+      "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==",
       "license": "ISC",
       "dependencies": {
         "reusify": "^1.0.4"
@@ -9311,13 +9404,12 @@
       }
     },
     "node_modules/happy-dom": {
-      "version": "15.11.7",
-      "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-15.11.7.tgz",
-      "integrity": "sha512-KyrFvnl+J9US63TEzwoiJOQzZBJY7KgBushJA8X61DMbNsH+2ONkDuLDnCnwUiPTF42tLoEmrPyoqbenVA5zrg==",
+      "version": "16.6.0",
+      "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-16.6.0.tgz",
+      "integrity": "sha512-Zz5S9sog8a3p8XYZbO+eI1QMOAvCNnIoyrH8A8MLX+X2mJrzADTy+kdETmc4q+uD9AGAvQYGn96qBAn2RAciKw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "entities": "^4.5.0",
         "webidl-conversions": "^7.0.0",
         "whatwg-mimetype": "^3.0.0"
       },
@@ -9361,6 +9453,13 @@
         "he": "bin/he"
       }
     },
+    "node_modules/hookified": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.6.0.tgz",
+      "integrity": "sha512-se7cpwTA+iA/eY548Bu03JJqBiEZAqU2jnyKdj5B5qurtBg64CZGHTgqCv4Yh7NWu6FGI09W61MCq+NoPj9GXA==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/hosted-git-info": {
       "version": "2.8.9",
       "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
@@ -9442,10 +9541,10 @@
       }
     },
     "node_modules/idiomorph": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/idiomorph/-/idiomorph-0.3.0.tgz",
-      "integrity": "sha512-UhV1Ey5xCxIwR9B+OgIjQa+1Jx99XQ1vQHUsKBU1RpQzCx1u+b+N6SOXgf5mEJDqemUI/ffccu6+71l2mJUsRA==",
-      "license": "BSD 2-Clause"
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/idiomorph/-/idiomorph-0.4.0.tgz",
+      "integrity": "sha512-VdXFpZOTXhLatJmhCWJR5oQKLXT01O6sFCJqT0/EqG71C4tYZdPJ5etvttwWsT2WKRYWz160XkNr1DUqXNMZHg==",
+      "license": "BSD-2-Clause"
     },
     "node_modules/ieee754": {
       "version": "1.2.1",
@@ -10024,9 +10123,9 @@
       "license": "MIT"
     },
     "node_modules/katex": {
-      "version": "0.16.18",
-      "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.18.tgz",
-      "integrity": "sha512-LRuk0rPdXrecAFwQucYjMiIs0JFefk6N1q/04mlw14aVIVgxq1FO0MA9RiIIGVaKOB5GIP5GH4aBBNraZERmaQ==",
+      "version": "0.16.20",
+      "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.20.tgz",
+      "integrity": "sha512-jjuLaMGD/7P8jUTpdKhA9IoqnH+yMFB3sdAFtq5QdAqeP2PjiSbnC3EaguKPNtv6dXXanHxp1ckwvF4a86LBig==",
       "funding": [
         "https://opencollective.com/katex",
         "https://github.com/sponsors/katex"
@@ -10506,9 +10605,9 @@
       }
     },
     "node_modules/markdownlint-cli/node_modules/glob": {
-      "version": "11.0.0",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz",
-      "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==",
+      "version": "11.0.1",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz",
+      "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==",
       "dev": true,
       "license": "ISC",
       "dependencies": {
@@ -10763,14 +10862,14 @@
       }
     },
     "node_modules/mlly": {
-      "version": "1.7.3",
-      "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.3.tgz",
-      "integrity": "sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==",
+      "version": "1.7.4",
+      "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz",
+      "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==",
       "license": "MIT",
       "dependencies": {
         "acorn": "^8.14.0",
-        "pathe": "^1.1.2",
-        "pkg-types": "^1.2.1",
+        "pathe": "^2.0.1",
+        "pkg-types": "^1.3.0",
         "ufo": "^1.5.4"
       }
     },
@@ -11138,9 +11237,9 @@
       "license": "BlueOak-1.0.0"
     },
     "node_modules/package-manager-detector": {
-      "version": "0.2.7",
-      "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.7.tgz",
-      "integrity": "sha512-g4+387DXDKlZzHkP+9FLt8yKj8+/3tOkPv7DVTJGGRm00RkEWgqbFstX1mXJ4M0VDYhUqsTOiISqNOJnhAu3PQ==",
+      "version": "0.2.8",
+      "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.8.tgz",
+      "integrity": "sha512-ts9KSdroZisdvKMWVAVCXiKqnqNfXz4+IbrBG8/BWx/TR5le+jfenvoBuIZ6UWM9nz47W7AbD9qYfAwfWMIwzA==",
       "license": "MIT"
     },
     "node_modules/parent-module": {
@@ -11257,9 +11356,9 @@
       }
     },
     "node_modules/pathe": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
-      "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.1.tgz",
+      "integrity": "sha512-6jpjMpOth5S9ITVu5clZ7NOgHNsv5vRQdheL9ztp2vZmM6fRbLvyua1tiBIL4lk8SAe3ARzeXEly6siXCjDHDw==",
       "license": "MIT"
     },
     "node_modules/pathval": {
@@ -11385,14 +11484,14 @@
       }
     },
     "node_modules/pkg-types": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.1.tgz",
-      "integrity": "sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==",
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
+      "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
       "license": "MIT",
       "dependencies": {
         "confbox": "^0.1.8",
-        "mlly": "^1.7.2",
-        "pathe": "^1.1.2"
+        "mlly": "^1.7.4",
+        "pathe": "^2.0.1"
       }
     },
     "node_modules/playwright": {
@@ -11464,9 +11563,9 @@
       }
     },
     "node_modules/postcss": {
-      "version": "8.4.49",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
-      "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
+      "version": "8.5.1",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
+      "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
       "funding": [
         {
           "type": "opencollective",
@@ -11483,7 +11582,7 @@
       ],
       "license": "MIT",
       "dependencies": {
-        "nanoid": "^3.3.7",
+        "nanoid": "^3.3.8",
         "picocolors": "^1.1.1",
         "source-map-js": "^1.2.1"
       },
@@ -11492,15 +11591,15 @@
       }
     },
     "node_modules/postcss-html": {
-      "version": "1.7.0",
-      "resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-1.7.0.tgz",
-      "integrity": "sha512-MfcMpSUIaR/nNgeVS8AyvyDugXlADjN9AcV7e5rDfrF1wduIAGSkL4q2+wgrZgA3sHVAHLDO9FuauHhZYW2nBw==",
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-1.8.0.tgz",
+      "integrity": "sha512-5mMeb1TgLWoRKxZ0Xh9RZDfwUUIqRrcxO2uXO+Ezl1N5lqpCiSU5Gk6+1kZediBfBHFtPCdopr2UZ2SgUsKcgQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "htmlparser2": "^8.0.0",
         "js-tokens": "^9.0.0",
-        "postcss": "^8.4.0",
+        "postcss": "^8.5.0",
         "postcss-safe-parser": "^6.0.0"
       },
       "engines": {
@@ -12322,9 +12421,9 @@
       }
     },
     "node_modules/resolve": {
-      "version": "1.22.9",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.9.tgz",
-      "integrity": "sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==",
+      "version": "1.22.10",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
+      "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
       "license": "MIT",
       "dependencies": {
         "is-core-module": "^2.16.0",
@@ -12334,6 +12433,9 @@
       "bin": {
         "resolve": "bin/resolve"
       },
+      "engines": {
+        "node": ">= 0.4"
+      },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
@@ -12584,18 +12686,18 @@
       }
     },
     "node_modules/seroval": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.1.1.tgz",
-      "integrity": "sha512-rqEO6FZk8mv7Hyv4UCj3FD3b6Waqft605TLfsCe/BiaylRpyyMC0b+uA5TJKawX3KzMrdi3wsLbCaLplrQmBvQ==",
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.2.0.tgz",
+      "integrity": "sha512-GURoU99ko2UiAgUC3qDCk59Jb3Ss4Po8VIMGkG8j5PFo2Q7y0YSMP8QG9NuL/fJCoTz9V1XZUbpNIMXPOfaGpA==",
       "license": "MIT",
       "engines": {
         "node": ">=10"
       }
     },
     "node_modules/seroval-plugins": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.1.1.tgz",
-      "integrity": "sha512-qNSy1+nUj7hsCOon7AO4wdAIo9P0jrzAMp18XhiOzA6/uO5TKtP7ScozVJ8T293oRIvi5wyCHSM4TrJo/c/GJA==",
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.2.0.tgz",
+      "integrity": "sha512-hULTbfzSe81jGWLH8TAJjkEvw6JWMqOo9Uq+4V4vg+HNq53hyHldM9ZOfjdzokcFysiTp9aFdV2vJpZFqKeDjQ==",
       "license": "MIT",
       "engines": {
         "node": ">=10"
@@ -12711,9 +12813,9 @@
       }
     },
     "node_modules/solid-js": {
-      "version": "1.9.3",
-      "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.3.tgz",
-      "integrity": "sha512-5ba3taPoZGt9GY3YlsCB24kCg0Lv/rie/HTD4kG6h4daZZz7+yK02xn8Vx8dLYBc9i6Ps5JwAbEiqjmKaLB3Ag==",
+      "version": "1.9.4",
+      "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.4.tgz",
+      "integrity": "sha512-ipQl8FJ31bFUoBNScDQTG3BjN6+9Rg+Q+f10bUbnO6EOTTf5NGerJeHc7wyu5I4RMHEl/WwZwUmy/PTRgxxZ8g==",
       "license": "MIT",
       "dependencies": {
         "csstype": "^3.1.0",
@@ -12829,9 +12931,9 @@
       }
     },
     "node_modules/spdx-license-ids": {
-      "version": "3.0.20",
-      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz",
-      "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==",
+      "version": "3.0.21",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz",
+      "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==",
       "license": "CC0-1.0"
     },
     "node_modules/spdx-ranges": {
@@ -13021,9 +13123,9 @@
       "license": "ISC"
     },
     "node_modules/stylelint": {
-      "version": "16.12.0",
-      "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.12.0.tgz",
-      "integrity": "sha512-F8zZ3L/rBpuoBZRvI4JVT20ZanPLXfQLzMOZg1tzPflRVh9mKpOZ8qcSIhh1my3FjAjZWG4T2POwGnmn6a6hbg==",
+      "version": "16.13.2",
+      "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.13.2.tgz",
+      "integrity": "sha512-wDlgh0mRO9RtSa3TdidqHd0nOG8MmUyVKl+dxA6C1j8aZRzpNeEgdhFmU5y4sZx4Fc6r46p0fI7p1vR5O2DZqA==",
       "dev": true,
       "funding": [
         {
@@ -13046,16 +13148,16 @@
         "colord": "^2.9.3",
         "cosmiconfig": "^9.0.0",
         "css-functions-list": "^3.2.3",
-        "css-tree": "^3.0.1",
+        "css-tree": "^3.1.0",
         "debug": "^4.3.7",
-        "fast-glob": "^3.3.2",
+        "fast-glob": "^3.3.3",
         "fastest-levenshtein": "^1.0.16",
-        "file-entry-cache": "^9.1.0",
+        "file-entry-cache": "^10.0.5",
         "global-modules": "^2.0.0",
         "globby": "^11.1.0",
         "globjoin": "^0.1.4",
         "html-tags": "^3.3.1",
-        "ignore": "^6.0.2",
+        "ignore": "^7.0.1",
         "imurmurhash": "^0.1.4",
         "is-plain-object": "^5.0.0",
         "known-css-properties": "^0.35.0",
@@ -13097,9 +13199,9 @@
       }
     },
     "node_modules/stylelint-declaration-strict-value": {
-      "version": "1.10.6",
-      "resolved": "https://registry.npmjs.org/stylelint-declaration-strict-value/-/stylelint-declaration-strict-value-1.10.6.tgz",
-      "integrity": "sha512-aZGEW4Ee26Tx4UvpQJbcElVXZ42EleujEByiyKDTT7t83EeSe9t0lAG3OOLJnnvLjz/dQnp+L+3IYTMeQI51vQ==",
+      "version": "1.10.7",
+      "resolved": "https://registry.npmjs.org/stylelint-declaration-strict-value/-/stylelint-declaration-strict-value-1.10.7.tgz",
+      "integrity": "sha512-FlMvc3uoQtMcItW3Zh8lHJ7oN2KGns3vZDCaTZoGFRiRIjImQoxO+6gAeRf+Dgi0nXFICIPq9xxFsMi8zuYUsg==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -13181,36 +13283,31 @@
       "license": "MIT"
     },
     "node_modules/stylelint/node_modules/file-entry-cache": {
-      "version": "9.1.0",
-      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-9.1.0.tgz",
-      "integrity": "sha512-/pqPFG+FdxWQj+/WSuzXSDaNzxgTLr/OrR1QuqfEZzDakpdYE70PwUxL7BPUa8hpjbvY1+qvCl8k+8Tq34xJgg==",
+      "version": "10.0.5",
+      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.0.5.tgz",
+      "integrity": "sha512-umpQsJrBNsdMDgreSryMEXvJh66XeLtZUwA8Gj7rHGearGufUFv6rB/bcXRFsiGWw/VeSUgUofF4Rf2UKEOrTA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "flat-cache": "^5.0.0"
-      },
-      "engines": {
-        "node": ">=18"
+        "flat-cache": "^6.1.5"
       }
     },
     "node_modules/stylelint/node_modules/flat-cache": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-5.0.0.tgz",
-      "integrity": "sha512-JrqFmyUl2PnPi1OvLyTVHnQvwQ0S+e6lGSwu8OkAZlSaNIZciTY2H/cOOROxsBA1m/LZNHDsqAgDZt6akWcjsQ==",
+      "version": "6.1.5",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.5.tgz",
+      "integrity": "sha512-QR+2kN38f8nMfiIQ1LHYjuDEmZNZVjxuxY+HufbS3BW0EX01Q5OnH7iduOYRutmgiXb797HAKcXUeXrvRjjgSQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "flatted": "^3.3.1",
-        "keyv": "^4.5.4"
-      },
-      "engines": {
-        "node": ">=18"
+        "cacheable": "^1.8.7",
+        "flatted": "^3.3.2",
+        "hookified": "^1.6.0"
       }
     },
     "node_modules/stylelint/node_modules/ignore": {
-      "version": "6.0.2",
-      "resolved": "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz",
-      "integrity": "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==",
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.3.tgz",
+      "integrity": "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -13269,9 +13366,9 @@
       }
     },
     "node_modules/stylis": {
-      "version": "4.3.4",
-      "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.4.tgz",
-      "integrity": "sha512-osIBl6BGUmSfDkyH2mB7EFvCJntXDrLhKjHTRj/rK6xLH0yuPrHULDRQzKokSOD4VoorhtKpfcfW1GAntu8now==",
+      "version": "4.3.5",
+      "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.5.tgz",
+      "integrity": "sha512-K7npNOKGRYuhAFFzkzMGfxFDpN6gDwf8hcMiE+uveTVbBgm93HrNP3ZDUpKqzZ4pG7TP6fmb+EMAQPjq9FqqvA==",
       "license": "MIT"
     },
     "node_modules/stylus": {
@@ -13740,9 +13837,9 @@
       "license": "MIT"
     },
     "node_modules/tinyexec": {
-      "version": "0.3.1",
-      "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.1.tgz",
-      "integrity": "sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==",
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+      "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
       "license": "MIT"
     },
     "node_modules/tinypool": {
@@ -13815,16 +13912,16 @@
       "license": "MIT"
     },
     "node_modules/ts-api-utils": {
-      "version": "1.4.3",
-      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
-      "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz",
+      "integrity": "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==",
       "dev": true,
       "license": "MIT",
       "engines": {
-        "node": ">=16"
+        "node": ">=18.12"
       },
       "peerDependencies": {
-        "typescript": ">=4.2.0"
+        "typescript": ">=4.8.4"
       }
     },
     "node_modules/ts-dedent": {
@@ -13889,9 +13986,9 @@
       }
     },
     "node_modules/type-fest": {
-      "version": "4.30.2",
-      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.30.2.tgz",
-      "integrity": "sha512-UJShLPYi1aWqCdq9HycOL/gwsuqda1OISdBO3t8RlXQC4QvtuIz4b5FCfe2dQIWEpmlRExKmcTBfP1r9bhY7ig==",
+      "version": "4.32.0",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.32.0.tgz",
+      "integrity": "sha512-rfgpoi08xagF3JSdtJlCwMq9DGNDE0IMh3Mkpc1wUypg9vPi786AiqeBBKcqvIkq42azsBM85N490fyZjeUftw==",
       "dev": true,
       "license": "(MIT OR CC0-1.0)",
       "engines": {
@@ -13902,9 +13999,9 @@
       }
     },
     "node_modules/typescript": {
-      "version": "5.7.2",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
-      "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
+      "version": "5.7.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
+      "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
       "license": "Apache-2.0",
       "bin": {
         "tsc": "bin/tsc",
@@ -14000,9 +14097,9 @@
       }
     },
     "node_modules/update-browserslist-db": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
-      "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz",
+      "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==",
       "funding": [
         {
           "type": "opencollective",
@@ -14020,7 +14117,7 @@
       "license": "MIT",
       "dependencies": {
         "escalade": "^3.2.0",
-        "picocolors": "^1.1.0"
+        "picocolors": "^1.1.1"
       },
       "bin": {
         "update-browserslist-db": "cli.js"
@@ -14197,6 +14294,13 @@
         "url": "https://opencollective.com/vitest"
       }
     },
+    "node_modules/vite-node/node_modules/pathe": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
+      "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/vite-string-plugin": {
       "version": "1.3.4",
       "resolved": "https://registry.npmjs.org/vite-string-plugin/-/vite-string-plugin-1.3.4.tgz",
@@ -14227,9 +14331,9 @@
       }
     },
     "node_modules/vite/node_modules/rollup": {
-      "version": "4.28.1",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz",
-      "integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==",
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.30.1.tgz",
+      "integrity": "sha512-mlJ4glW020fPuLi7DkM/lN97mYEZGWeqBnrljzN0gs7GLctqX3lNWxKQ7Gl712UAX+6fog/L3jh4gb7R6aVi3w==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -14243,25 +14347,25 @@
         "npm": ">=8.0.0"
       },
       "optionalDependencies": {
-        "@rollup/rollup-android-arm-eabi": "4.28.1",
-        "@rollup/rollup-android-arm64": "4.28.1",
-        "@rollup/rollup-darwin-arm64": "4.28.1",
-        "@rollup/rollup-darwin-x64": "4.28.1",
-        "@rollup/rollup-freebsd-arm64": "4.28.1",
-        "@rollup/rollup-freebsd-x64": "4.28.1",
-        "@rollup/rollup-linux-arm-gnueabihf": "4.28.1",
-        "@rollup/rollup-linux-arm-musleabihf": "4.28.1",
-        "@rollup/rollup-linux-arm64-gnu": "4.28.1",
-        "@rollup/rollup-linux-arm64-musl": "4.28.1",
-        "@rollup/rollup-linux-loongarch64-gnu": "4.28.1",
-        "@rollup/rollup-linux-powerpc64le-gnu": "4.28.1",
-        "@rollup/rollup-linux-riscv64-gnu": "4.28.1",
-        "@rollup/rollup-linux-s390x-gnu": "4.28.1",
-        "@rollup/rollup-linux-x64-gnu": "4.28.1",
-        "@rollup/rollup-linux-x64-musl": "4.28.1",
-        "@rollup/rollup-win32-arm64-msvc": "4.28.1",
-        "@rollup/rollup-win32-ia32-msvc": "4.28.1",
-        "@rollup/rollup-win32-x64-msvc": "4.28.1",
+        "@rollup/rollup-android-arm-eabi": "4.30.1",
+        "@rollup/rollup-android-arm64": "4.30.1",
+        "@rollup/rollup-darwin-arm64": "4.30.1",
+        "@rollup/rollup-darwin-x64": "4.30.1",
+        "@rollup/rollup-freebsd-arm64": "4.30.1",
+        "@rollup/rollup-freebsd-x64": "4.30.1",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.30.1",
+        "@rollup/rollup-linux-arm-musleabihf": "4.30.1",
+        "@rollup/rollup-linux-arm64-gnu": "4.30.1",
+        "@rollup/rollup-linux-arm64-musl": "4.30.1",
+        "@rollup/rollup-linux-loongarch64-gnu": "4.30.1",
+        "@rollup/rollup-linux-powerpc64le-gnu": "4.30.1",
+        "@rollup/rollup-linux-riscv64-gnu": "4.30.1",
+        "@rollup/rollup-linux-s390x-gnu": "4.30.1",
+        "@rollup/rollup-linux-x64-gnu": "4.30.1",
+        "@rollup/rollup-linux-x64-musl": "4.30.1",
+        "@rollup/rollup-win32-arm64-msvc": "4.30.1",
+        "@rollup/rollup-win32-ia32-msvc": "4.30.1",
+        "@rollup/rollup-win32-x64-msvc": "4.30.1",
         "fsevents": "~2.3.2"
       }
     },
@@ -14341,6 +14445,13 @@
         "@jridgewell/sourcemap-codec": "^1.5.0"
       }
     },
+    "node_modules/vitest/node_modules/pathe": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
+      "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/vscode-jsonrpc": {
       "version": "8.2.0",
       "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
@@ -14595,42 +14706,39 @@
       }
     },
     "node_modules/webpack-cli": {
-      "version": "5.1.4",
-      "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz",
-      "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-6.0.1.tgz",
+      "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==",
       "license": "MIT",
       "dependencies": {
-        "@discoveryjs/json-ext": "^0.5.0",
-        "@webpack-cli/configtest": "^2.1.1",
-        "@webpack-cli/info": "^2.0.2",
-        "@webpack-cli/serve": "^2.0.5",
+        "@discoveryjs/json-ext": "^0.6.1",
+        "@webpack-cli/configtest": "^3.0.1",
+        "@webpack-cli/info": "^3.0.1",
+        "@webpack-cli/serve": "^3.0.1",
         "colorette": "^2.0.14",
-        "commander": "^10.0.1",
+        "commander": "^12.1.0",
         "cross-spawn": "^7.0.3",
-        "envinfo": "^7.7.3",
+        "envinfo": "^7.14.0",
         "fastest-levenshtein": "^1.0.12",
         "import-local": "^3.0.2",
         "interpret": "^3.1.1",
         "rechoir": "^0.8.0",
-        "webpack-merge": "^5.7.3"
+        "webpack-merge": "^6.0.1"
       },
       "bin": {
         "webpack-cli": "bin/cli.js"
       },
       "engines": {
-        "node": ">=14.15.0"
+        "node": ">=18.12.0"
       },
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/webpack"
       },
       "peerDependencies": {
-        "webpack": "5.x.x"
+        "webpack": "^5.82.0"
       },
       "peerDependenciesMeta": {
-        "@webpack-cli/generators": {
-          "optional": true
-        },
         "webpack-bundle-analyzer": {
           "optional": true
         },
@@ -14640,26 +14748,26 @@
       }
     },
     "node_modules/webpack-cli/node_modules/commander": {
-      "version": "10.0.1",
-      "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
-      "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
+      "version": "12.1.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
+      "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
       "license": "MIT",
       "engines": {
-        "node": ">=14"
+        "node": ">=18"
       }
     },
     "node_modules/webpack-merge": {
-      "version": "5.10.0",
-      "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz",
-      "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz",
+      "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==",
       "license": "MIT",
       "dependencies": {
         "clone-deep": "^4.0.1",
         "flat": "^5.0.2",
-        "wildcard": "^2.0.0"
+        "wildcard": "^2.0.1"
       },
       "engines": {
-        "node": ">=10.0.0"
+        "node": ">=18.0.0"
       }
     },
     "node_modules/webpack-sources": {
@@ -14977,9 +15085,9 @@
       "license": "ISC"
     },
     "node_modules/yaml": {
-      "version": "2.6.1",
-      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz",
-      "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==",
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz",
+      "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==",
       "license": "ISC",
       "bin": {
         "yaml": "bin.mjs"
diff --git a/package.json b/package.json
index 31a65c647c..2cda2ae844 100644
--- a/package.json
+++ b/package.json
@@ -5,18 +5,18 @@
   },
   "dependencies": {
     "@citation-js/core": "0.7.14",
-    "@citation-js/plugin-bibtex": "0.7.16",
+    "@citation-js/plugin-bibtex": "0.7.17",
     "@citation-js/plugin-csl": "0.7.14",
     "@citation-js/plugin-software-formats": "0.6.1",
     "@github/markdown-toolbar-element": "2.2.3",
-    "@github/relative-time-element": "4.4.4",
+    "@github/relative-time-element": "4.4.5",
     "@github/text-expander-element": "2.8.0",
     "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
     "@primer/octicons": "19.14.0",
     "@silverwind/vue3-calendar-heatmap": "2.0.6",
     "add-asset-webpack-plugin": "3.0.0",
     "ansi_up": "6.0.2",
-    "asciinema-player": "3.8.1",
+    "asciinema-player": "3.8.2",
     "chart.js": "4.4.7",
     "chartjs-adapter-dayjs-4": "1.0.4",
     "chartjs-plugin-zoom": "2.2.0",
@@ -28,11 +28,11 @@
     "easymde": "2.18.0",
     "esbuild-loader": "4.2.2",
     "escape-goat": "4.0.0",
-    "fast-glob": "3.3.2",
+    "fast-glob": "3.3.3",
     "htmx.org": "2.0.4",
-    "idiomorph": "0.3.0",
+    "idiomorph": "0.4.0",
     "jquery": "3.7.1",
-    "katex": "0.16.18",
+    "katex": "0.16.20",
     "license-checker-webpack-plugin": "0.2.1",
     "mermaid": "11.4.1",
     "mini-css-extract-plugin": "2.9.2",
@@ -41,7 +41,7 @@
     "monaco-editor-webpack-plugin": "7.1.0",
     "pdfobject": "2.3.0",
     "perfect-debounce": "1.0.0",
-    "postcss": "8.4.49",
+    "postcss": "8.5.1",
     "postcss-loader": "8.1.1",
     "postcss-nesting": "13.0.1",
     "sortablejs": "1.15.6",
@@ -52,7 +52,7 @@
     "tippy.js": "6.3.7",
     "toastify-js": "1.12.0",
     "tributejs": "5.1.3",
-    "typescript": "5.7.2",
+    "typescript": "5.7.3",
     "uint8-to-base64": "0.2.0",
     "vanilla-colorful": "0.7.2",
     "vue": "3.5.13",
@@ -60,14 +60,14 @@
     "vue-chartjs": "5.3.2",
     "vue-loader": "17.4.2",
     "webpack": "5.97.1",
-    "webpack-cli": "5.1.4",
+    "webpack-cli": "6.0.1",
     "wrap-ansi": "9.0.0"
   },
   "devDependencies": {
     "@eslint-community/eslint-plugin-eslint-comments": "4.4.1",
     "@playwright/test": "1.49.1",
     "@stoplight/spectral-cli": "6.14.2",
-    "@stylistic/eslint-plugin-js": "2.12.1",
+    "@stylistic/eslint-plugin-js": "2.13.0",
     "@stylistic/stylelint-plugin": "3.1.1",
     "@types/dropzone": "5.7.9",
     "@types/jquery": "3.5.32",
@@ -79,8 +79,8 @@
     "@types/throttle-debounce": "5.0.2",
     "@types/tinycolor2": "1.4.6",
     "@types/toastify-js": "1.12.3",
-    "@typescript-eslint/eslint-plugin": "8.18.1",
-    "@typescript-eslint/parser": "8.18.1",
+    "@typescript-eslint/eslint-plugin": "8.20.0",
+    "@typescript-eslint/parser": "8.20.0",
     "@vitejs/plugin-vue": "5.2.1",
     "eslint": "8.57.0",
     "eslint-import-resolver-typescript": "3.7.0",
@@ -98,16 +98,16 @@
     "eslint-plugin-vue": "9.32.0",
     "eslint-plugin-vue-scoped-css": "2.9.0",
     "eslint-plugin-wc": "2.2.0",
-    "happy-dom": "15.11.7",
+    "happy-dom": "16.6.0",
     "markdownlint-cli": "0.43.0",
     "nolyfill": "1.0.43",
-    "postcss-html": "1.7.0",
-    "stylelint": "16.12.0",
+    "postcss-html": "1.8.0",
+    "stylelint": "16.13.2",
     "stylelint-declaration-block-no-ignored-properties": "2.8.0",
-    "stylelint-declaration-strict-value": "1.10.6",
+    "stylelint-declaration-strict-value": "1.10.7",
     "stylelint-value-no-unknown-custom-properties": "6.0.1",
     "svgo": "3.3.2",
-    "type-fest": "4.30.2",
+    "type-fest": "4.32.0",
     "updates": "16.4.1",
     "vite-string-plugin": "1.3.4",
     "vitest": "2.1.8",
diff --git a/poetry.lock b/poetry.lock
index 5b9029eb8c..8c01674966 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,14 +1,14 @@
-# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand.
 
 [[package]]
 name = "click"
-version = "8.1.7"
+version = "8.1.8"
 description = "Composable command line interface toolkit"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
-    {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
+    {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"},
+    {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"},
 ]
 
 [package.dependencies]
@@ -42,33 +42,33 @@ six = ">=1.13.0"
 
 [[package]]
 name = "djlint"
-version = "1.36.3"
+version = "1.36.4"
 description = "HTML Template Linter and Formatter"
 optional = false
 python-versions = ">=3.9"
 files = [
-    {file = "djlint-1.36.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ae7c620b58e16d6bf003bd7de3f71376a7a3daa79dc02e77f3726d5a75243f2"},
-    {file = "djlint-1.36.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e155ce0970d4a28d0a2e9f2e106733a2ad05910eee90e056b056d48049e4a97b"},
-    {file = "djlint-1.36.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e8bb0406e60cc696806aa6226df137618f3889c72f2dbdfa76c908c99151579"},
-    {file = "djlint-1.36.3-cp310-cp310-win_amd64.whl", hash = "sha256:76d32faf988ad58ef2e7a11d04046fc984b98391761bf1b61f9a6044da53d414"},
-    {file = "djlint-1.36.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:32f7a5834000fff22e94d1d35f95aaf2e06f2af2cae18af0ed2a4e215d60e730"},
-    {file = "djlint-1.36.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3eb1b9c0be499e63e8822a051e7e55f188ff1ab8172a85d338a8ae21c872060e"},
-    {file = "djlint-1.36.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c2e0dd1f26eb472b8c84eb70d6482877b6497a1fd031d7534864088f016d5ea"},
-    {file = "djlint-1.36.3-cp311-cp311-win_amd64.whl", hash = "sha256:a06b531ab9d049c46ad4d2365d1857004a1a9dd0c23c8eae94aa0d233c6ec00d"},
-    {file = "djlint-1.36.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e66361a865e5e5a4bbcb40f56af7f256fd02cbf9d48b763a40172749cc294084"},
-    {file = "djlint-1.36.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:36e102b80d83e9ac2e6be9a9ded32fb925945f6dbc7a7156e4415de1b0aa0dba"},
-    {file = "djlint-1.36.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ac4b7370d80bd82281e57a470de8923ac494ffb571b89d8787cef57c738c69a"},
-    {file = "djlint-1.36.3-cp312-cp312-win_amd64.whl", hash = "sha256:107cc56bbef13d60cc0ae774a4d52881bf98e37c02412e573827a3e549217e3a"},
-    {file = "djlint-1.36.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2a9f51971d6e63c41ea9b3831c928e1f21ae6fe57e87a3452cfe672d10232433"},
-    {file = "djlint-1.36.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:080c98714b55d8f0fef5c42beaee8247ebb2e3d46b0936473bd6c47808bb6302"},
-    {file = "djlint-1.36.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f65a80e0b5cb13d357ea51ca6570b34c2d9d18974c1e57142de760ea27d49ed0"},
-    {file = "djlint-1.36.3-cp313-cp313-win_amd64.whl", hash = "sha256:95ef6b67ef7f2b90d9434bba37d572031079001dc8524add85c00ef0386bda1e"},
-    {file = "djlint-1.36.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e2317a32094d525bc41cd11c8dc064bf38d1b442c99cc3f7c4a2616b5e6ce6e"},
-    {file = "djlint-1.36.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e82266c28793cd15f97b93535d72bfbc77306eaaf6b210dd90910383a814ee6c"},
-    {file = "djlint-1.36.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01b2101c2d1b079e8d545e6d9d03487fcca14d2371e44cbfdedee15b0bf4567c"},
-    {file = "djlint-1.36.3-cp39-cp39-win_amd64.whl", hash = "sha256:15cde63ef28beb5194ff4137883025f125676ece1b574b64a3e1c6daed734639"},
-    {file = "djlint-1.36.3-py3-none-any.whl", hash = "sha256:0c05cd5b76785de2c41a2420c06ffd112800bfc0f9c0f399cc7cea7c42557f4c"},
-    {file = "djlint-1.36.3.tar.gz", hash = "sha256:d85735da34bc7ac93ad8ef9b4822cc2a23d5f0ce33f25438737b8dca1d404f78"},
+    {file = "djlint-1.36.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a2dfb60883ceb92465201bfd392291a7597c6752baede6fbb6f1980cac8d6c5c"},
+    {file = "djlint-1.36.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4bc6a1320c0030244b530ac200642f883d3daa451a115920ef3d56d08b644292"},
+    {file = "djlint-1.36.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3164a048c7bb0baf042387b1e33f9bbbf99d90d1337bb4c3d66eb0f96f5400a1"},
+    {file = "djlint-1.36.4-cp310-cp310-win_amd64.whl", hash = "sha256:3196d5277da5934962d67ad6c33a948ba77a7b6eadf064648bef6ee5f216b03c"},
+    {file = "djlint-1.36.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d68da0ed10ee9ca1e32e225cbb8e9b98bf7e6f8b48a8e4836117b6605b88cc7"},
+    {file = "djlint-1.36.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c0478d5392247f1e6ee29220bbdbf7fb4e1bc0e7e83d291fda6fb926c1787ba7"},
+    {file = "djlint-1.36.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:962f7b83aee166e499eff916d631c6dde7f1447d7610785a60ed2a75a5763483"},
+    {file = "djlint-1.36.4-cp311-cp311-win_amd64.whl", hash = "sha256:53cbc450aa425c832f09bc453b8a94a039d147b096740df54a3547fada77ed08"},
+    {file = "djlint-1.36.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff9faffd7d43ac20467493fa71d5355b5b330a00ade1c4d1e859022f4195223b"},
+    {file = "djlint-1.36.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:79489e262b5ac23a8dfb7ca37f1eea979674cfc2d2644f7061d95bea12c38f7e"},
+    {file = "djlint-1.36.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e58c5fa8c6477144a0be0a87273706a059e6dd0d6efae01146ae8c29cdfca675"},
+    {file = "djlint-1.36.4-cp312-cp312-win_amd64.whl", hash = "sha256:bb6903777bf3124f5efedcddf1f4716aef097a7ec4223fc0fa54b865829a6e08"},
+    {file = "djlint-1.36.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ead475013bcac46095b1bbc8cf97ed2f06e83422335734363f8a76b4ba7e47c2"},
+    {file = "djlint-1.36.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6c601dfa68ea253311deb4a29a7362b7a64933bdfcfb5a06618f3e70ad1fa835"},
+    {file = "djlint-1.36.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bda5014f295002363381969864addeb2db13955f1b26e772657c3b273ed7809f"},
+    {file = "djlint-1.36.4-cp313-cp313-win_amd64.whl", hash = "sha256:16ce37e085afe5a30953b2bd87cbe34c37843d94c701fc68a2dda06c1e428ff4"},
+    {file = "djlint-1.36.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:89678661888c03d7bc6cadd75af69db29962b5ecbf93a81518262f5c48329f04"},
+    {file = "djlint-1.36.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b01a98df3e1ab89a552793590875bc6e954cad661a9304057db75363d519fa0"},
+    {file = "djlint-1.36.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dabbb4f7b93223d471d09ae34ed515fef98b2233cbca2449ad117416c44b1351"},
+    {file = "djlint-1.36.4-cp39-cp39-win_amd64.whl", hash = "sha256:7a483390d17e44df5bc23dcea29bdf6b63f3ed8b4731d844773a4829af4f5e0b"},
+    {file = "djlint-1.36.4-py3-none-any.whl", hash = "sha256:e9699b8ac3057a6ed04fb90835b89bee954ed1959c01541ce4f8f729c938afdd"},
+    {file = "djlint-1.36.4.tar.gz", hash = "sha256:17254f218b46fe5a714b224c85074c099bcb74e3b2e1f15c2ddc2cf415a408a1"},
 ]
 
 [package.dependencies]
@@ -82,15 +82,17 @@ pyyaml = ">=6"
 regex = ">=2023"
 tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
 tqdm = ">=4.62.2"
+typing-extensions = {version = ">=3.6.6", markers = "python_version < \"3.11\""}
 
 [[package]]
 name = "editorconfig"
-version = "0.12.4"
+version = "0.17.0"
 description = "EditorConfig File Locator and Interpreter for Python"
 optional = false
 python-versions = "*"
 files = [
-    {file = "EditorConfig-0.12.4.tar.gz", hash = "sha256:24857fa1793917dd9ccf0c7810a07e05404ce9b823521c7dce22a4fb5d125f80"},
+    {file = "EditorConfig-0.17.0-py3-none-any.whl", hash = "sha256:fe491719c5f65959ec00b167d07740e7ffec9a3f362038c72b289330b9991dfc"},
+    {file = "editorconfig-0.17.0.tar.gz", hash = "sha256:8739052279699840065d3a9f5c125d7d5a98daeefe53b0e5274261d77cb49aa2"},
 ]
 
 [[package]]
@@ -370,6 +372,17 @@ notebook = ["ipywidgets (>=6)"]
 slack = ["slack-sdk"]
 telegram = ["requests"]
 
+[[package]]
+name = "typing-extensions"
+version = "4.12.2"
+description = "Backported and Experimental Type Hints for Python 3.8+"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
+    {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
+]
+
 [[package]]
 name = "yamllint"
 version = "1.35.1"
@@ -391,4 +404,4 @@ dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"]
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.10"
-content-hash = "01b1e2f910276dd20a70ebb665c83415c37531709d90874f5b7a86a5305e2369"
+content-hash = "f2e8260efe6e25f77ef387daff9551e41d25027e4794b42bc7a851ed0dfafd85"
diff --git a/pyproject.toml b/pyproject.toml
index 4d36b30ea7..851504e72b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,7 +5,7 @@ package-mode = false
 python = "^3.10"
 
 [tool.poetry.group.dev.dependencies]
-djlint = "1.36.3"
+djlint = "1.36.4"
 yamllint = "1.35.1"
 
 [tool.djlint]
diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go
index 592879235c..a5ea752cf1 100644
--- a/routers/api/v1/repo/branch.go
+++ b/routers/api/v1/repo/branch.go
@@ -12,6 +12,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	git_model "code.gitea.io/gitea/models/git"
 	"code.gitea.io/gitea/models/organization"
+	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
@@ -443,7 +444,14 @@ func UpdateBranch(ctx *context.APIContext) {
 
 	msg, err := repo_service.RenameBranch(ctx, repo, ctx.Doer, ctx.Repo.GitRepo, oldName, opt.Name)
 	if err != nil {
-		ctx.Error(http.StatusInternalServerError, "RenameBranch", err)
+		switch {
+		case repo_model.IsErrUserDoesNotHaveAccessToRepo(err):
+			ctx.Error(http.StatusForbidden, "", "User must be a repo or site admin to rename default or protected branches.")
+		case errors.Is(err, git_model.ErrBranchIsProtected):
+			ctx.Error(http.StatusForbidden, "", "Branch is protected by glob-based protection rules.")
+		default:
+			ctx.Error(http.StatusInternalServerError, "RenameBranch", err)
+		}
 		return
 	}
 	if msg == "target_exist" {
diff --git a/routers/api/v1/repo/hook_test.go b/routers/api/v1/repo/hook_test.go
index c659a16f54..2d15c6e078 100644
--- a/routers/api/v1/repo/hook_test.go
+++ b/routers/api/v1/repo/hook_test.go
@@ -23,7 +23,7 @@ func TestTestHook(t *testing.T) {
 	contexttest.LoadRepoCommit(t, ctx)
 	contexttest.LoadUser(t, ctx, 2)
 	TestHook(ctx)
-	assert.EqualValues(t, http.StatusNoContent, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusNoContent, ctx.Resp.WrittenStatus())
 
 	unittest.AssertExistsAndLoadBean(t, &webhook.HookTask{
 		HookID: 1,
diff --git a/routers/api/v1/repo/repo_test.go b/routers/api/v1/repo/repo_test.go
index 8d6ca9e3b5..0a63b16a99 100644
--- a/routers/api/v1/repo/repo_test.go
+++ b/routers/api/v1/repo/repo_test.go
@@ -58,7 +58,7 @@ func TestRepoEdit(t *testing.T) {
 	web.SetForm(ctx, &opts)
 	Edit(ctx)
 
-	assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
 	unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
 		ID: 1,
 	}, unittest.Cond("name = ? AND is_archived = 1", *opts.Name))
@@ -78,7 +78,7 @@ func TestRepoEditNameChange(t *testing.T) {
 
 	web.SetForm(ctx, &opts)
 	Edit(ctx)
-	assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
 
 	unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
 		ID: 1,
diff --git a/routers/common/middleware.go b/routers/common/middleware.go
index 12b0c67b01..2ba02de8ed 100644
--- a/routers/common/middleware.go
+++ b/routers/common/middleware.go
@@ -9,6 +9,7 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/modules/cache"
+	"code.gitea.io/gitea/modules/gtprof"
 	"code.gitea.io/gitea/modules/httplib"
 	"code.gitea.io/gitea/modules/reqctx"
 	"code.gitea.io/gitea/modules/setting"
@@ -43,14 +44,26 @@ func ProtocolMiddlewares() (handlers []any) {
 
 func RequestContextHandler() func(h http.Handler) http.Handler {
 	return func(next http.Handler) http.Handler {
-		return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
-			profDesc := fmt.Sprintf("%s: %s", req.Method, req.RequestURI)
+		return http.HandlerFunc(func(respOrig http.ResponseWriter, req *http.Request) {
+			// this response writer might not be the same as the one in context.Base.Resp
+			// because there might be a "gzip writer" in the middle, so the "written size" here is the compressed size
+			respWriter := context.WrapResponseWriter(respOrig)
+
+			profDesc := fmt.Sprintf("HTTP: %s %s", req.Method, req.RequestURI)
 			ctx, finished := reqctx.NewRequestContext(req.Context(), profDesc)
 			defer finished()
 
+			ctx, span := gtprof.GetTracer().Start(ctx, gtprof.TraceSpanHTTP)
+			req = req.WithContext(ctx)
+			defer func() {
+				chiCtx := chi.RouteContext(req.Context())
+				span.SetAttributeString(gtprof.TraceAttrHTTPRoute, chiCtx.RoutePattern())
+				span.End()
+			}()
+
 			defer func() {
 				if err := recover(); err != nil {
-					RenderPanicErrorPage(resp, req, err) // it should never panic
+					RenderPanicErrorPage(respWriter, req, err) // it should never panic
 				}
 			}()
 
@@ -62,7 +75,7 @@ func RequestContextHandler() func(h http.Handler) http.Handler {
 					_ = req.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory
 				}
 			})
-			next.ServeHTTP(context.WrapResponseWriter(resp), req)
+			next.ServeHTTP(respWriter, req)
 		})
 	}
 }
@@ -71,11 +84,11 @@ func ChiRoutePathHandler() func(h http.Handler) http.Handler {
 	// make sure chi uses EscapedPath(RawPath) as RoutePath, then "%2f" could be handled correctly
 	return func(next http.Handler) http.Handler {
 		return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
-			ctx := chi.RouteContext(req.Context())
+			chiCtx := chi.RouteContext(req.Context())
 			if req.URL.RawPath == "" {
-				ctx.RoutePath = req.URL.EscapedPath()
+				chiCtx.RoutePath = req.URL.EscapedPath()
 			} else {
-				ctx.RoutePath = req.URL.RawPath
+				chiCtx.RoutePath = req.URL.RawPath
 			}
 			next.ServeHTTP(resp, req)
 		})
diff --git a/routers/init.go b/routers/init.go
index e7aa765bf0..744feee2f0 100644
--- a/routers/init.go
+++ b/routers/init.go
@@ -213,7 +213,7 @@ func NormalRoutes() *web.Router {
 	}
 
 	r.NotFound(func(w http.ResponseWriter, req *http.Request) {
-		routing.UpdateFuncInfo(req.Context(), routing.GetFuncInfo(http.NotFound, "GlobalNotFound"))
+		defer routing.RecordFuncInfo(req.Context(), routing.GetFuncInfo(http.NotFound, "GlobalNotFound"))()
 		http.NotFound(w, req)
 	})
 	return r
diff --git a/routers/web/admin/admin.go b/routers/web/admin/admin.go
index 3902a1efb1..0cd13acf60 100644
--- a/routers/web/admin/admin.go
+++ b/routers/web/admin/admin.go
@@ -37,6 +37,7 @@ const (
 	tplSelfCheck    templates.TplName = "admin/self_check"
 	tplCron         templates.TplName = "admin/cron"
 	tplQueue        templates.TplName = "admin/queue"
+	tplPerfTrace    templates.TplName = "admin/perftrace"
 	tplStacktrace   templates.TplName = "admin/stacktrace"
 	tplQueueManage  templates.TplName = "admin/queue_manage"
 	tplStats        templates.TplName = "admin/stats"
diff --git a/routers/web/admin/diagnosis.go b/routers/web/admin/diagnosis.go
index 020554a35a..d040dbe0ba 100644
--- a/routers/web/admin/diagnosis.go
+++ b/routers/web/admin/diagnosis.go
@@ -10,13 +10,15 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/modules/httplib"
+	"code.gitea.io/gitea/modules/tailmsg"
+	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/services/context"
 )
 
 func MonitorDiagnosis(ctx *context.Context) {
 	seconds := ctx.FormInt64("seconds")
-	if seconds <= 5 {
-		seconds = 5
+	if seconds <= 1 {
+		seconds = 1
 	}
 	if seconds > 300 {
 		seconds = 300
@@ -65,4 +67,16 @@ func MonitorDiagnosis(ctx *context.Context) {
 		return
 	}
 	_ = pprof.Lookup("heap").WriteTo(f, 0)
+
+	f, err = zipWriter.CreateHeader(&zip.FileHeader{Name: "perftrace.txt", Method: zip.Deflate, Modified: time.Now()})
+	if err != nil {
+		ctx.ServerError("Failed to create zip file", err)
+		return
+	}
+	for _, record := range tailmsg.GetManager().GetTraceRecorder().GetRecords() {
+		_, _ = f.Write(util.UnsafeStringToBytes(record.Time.Format(time.RFC3339)))
+		_, _ = f.Write([]byte(" "))
+		_, _ = f.Write(util.UnsafeStringToBytes((record.Content)))
+		_, _ = f.Write([]byte("\n\n"))
+	}
 }
diff --git a/routers/web/admin/perftrace.go b/routers/web/admin/perftrace.go
new file mode 100644
index 0000000000..51ee57da10
--- /dev/null
+++ b/routers/web/admin/perftrace.go
@@ -0,0 +1,18 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package admin
+
+import (
+	"net/http"
+
+	"code.gitea.io/gitea/modules/tailmsg"
+	"code.gitea.io/gitea/services/context"
+)
+
+func PerfTrace(ctx *context.Context) {
+	monitorTraceCommon(ctx)
+	ctx.Data["PageIsAdminMonitorPerfTrace"] = true
+	ctx.Data["PerfTraceRecords"] = tailmsg.GetManager().GetTraceRecorder().GetRecords()
+	ctx.HTML(http.StatusOK, tplPerfTrace)
+}
diff --git a/routers/web/admin/stacktrace.go b/routers/web/admin/stacktrace.go
index ff751be621..2b8c2fb4af 100644
--- a/routers/web/admin/stacktrace.go
+++ b/routers/web/admin/stacktrace.go
@@ -12,10 +12,17 @@ import (
 	"code.gitea.io/gitea/services/context"
 )
 
+func monitorTraceCommon(ctx *context.Context) {
+	ctx.Data["Title"] = ctx.Tr("admin.monitor")
+	ctx.Data["PageIsAdminMonitorTrace"] = true
+	// Hide the performance trace tab in production, because it shows a lot of SQLs and is not that useful for end users.
+	// To avoid confusing end users, do not let them know this tab. End users should "download diagnosis report" instead.
+	ctx.Data["ShowAdminPerformanceTraceTab"] = !setting.IsProd
+}
+
 // Stacktrace show admin monitor goroutines page
 func Stacktrace(ctx *context.Context) {
-	ctx.Data["Title"] = ctx.Tr("admin.monitor")
-	ctx.Data["PageIsAdminMonitorStacktrace"] = true
+	monitorTraceCommon(ctx)
 
 	ctx.Data["GoroutineCount"] = runtime.NumGoroutine()
 
diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go
index 147d8d3802..9525e19554 100644
--- a/routers/web/auth/linkaccount.go
+++ b/routers/web/auth/linkaccount.go
@@ -29,6 +29,7 @@ var tplLinkAccount templates.TplName = "user/auth/link_account"
 
 // LinkAccount shows the page where the user can decide to login or create a new account
 func LinkAccount(ctx *context.Context) {
+	// FIXME: these common template variables should be prepared in one common function, but not just copy-paste again and again.
 	ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
 	ctx.Data["Title"] = ctx.Tr("link_account")
 	ctx.Data["LinkAccountMode"] = true
@@ -43,6 +44,7 @@ func LinkAccount(ctx *context.Context) {
 	ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey
 	ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
 	ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
+	ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
 	ctx.Data["ShowRegistrationButton"] = false
 
 	// use this to set the right link into the signIn and signUp templates in the link_account template
@@ -50,6 +52,11 @@ func LinkAccount(ctx *context.Context) {
 	ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
 
 	gothUser, ok := ctx.Session.Get("linkAccountGothUser").(goth.User)
+
+	// If you'd like to quickly debug the "link account" page layout, just uncomment the blow line
+	// Don't worry, when the below line exists, the lint won't pass: ineffectual assignment to gothUser (ineffassign)
+	// gothUser, ok = goth.User{Email: "invalid-email", Name: "."}, true // intentionally use invalid data to avoid pass the registration check
+
 	if !ok {
 		// no account in session, so just redirect to the login page, then the user could restart the process
 		ctx.Redirect(setting.AppSubURL + "/user/login")
@@ -135,6 +142,8 @@ func LinkAccountPostSignIn(ctx *context.Context) {
 	ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
 	ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey
 	ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
+	ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
+	ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
 	ctx.Data["ShowRegistrationButton"] = false
 
 	// use this to set the right link into the signIn and signUp templates in the link_account template
@@ -223,6 +232,8 @@ func LinkAccountPostRegister(ctx *context.Context) {
 	ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
 	ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey
 	ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
+	ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
+	ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
 	ctx.Data["ShowRegistrationButton"] = false
 
 	// use this to set the right link into the signIn and signUp templates in the link_account template
diff --git a/routers/web/base.go b/routers/web/base.go
index aa0b43c16a..abe11593f7 100644
--- a/routers/web/base.go
+++ b/routers/web/base.go
@@ -34,7 +34,7 @@ func storageHandler(storageSetting *setting.Storage, prefix string, objStore sto
 				http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
 				return
 			}
-			routing.UpdateFuncInfo(req.Context(), funcInfo)
+			defer routing.RecordFuncInfo(req.Context(), funcInfo)()
 
 			rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/")
 			rPath = util.PathJoinRelX(rPath)
@@ -65,7 +65,7 @@ func storageHandler(storageSetting *setting.Storage, prefix string, objStore sto
 			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
 			return
 		}
-		routing.UpdateFuncInfo(req.Context(), funcInfo)
+		defer routing.RecordFuncInfo(req.Context(), funcInfo)()
 
 		rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/")
 		rPath = util.PathJoinRelX(rPath)
diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go
index 9a18ca5305..e5d83960b8 100644
--- a/routers/web/repo/actions/view.go
+++ b/routers/web/repo/actions/view.go
@@ -850,7 +850,7 @@ func Run(ctx *context_module.Context) {
 	inputs := make(map[string]any)
 	if workflowDispatch := workflow.WorkflowDispatchConfig(); workflowDispatch != nil {
 		for name, config := range workflowDispatch.Inputs {
-			value := ctx.Req.PostForm.Get(name)
+			value := ctx.Req.PostFormValue(name)
 			if config.Type == "boolean" {
 				// https://www.w3.org/TR/html401/interact/forms.html
 				// https://stackoverflow.com/questions/11424037/do-checkbox-inputs-only-post-data-if-theyre-checked
diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go
index 5011cbfc77..8747526f72 100644
--- a/routers/web/repo/branch.go
+++ b/routers/web/repo/branch.go
@@ -37,7 +37,6 @@ const (
 // Branches render repository branch page
 func Branches(ctx *context.Context) {
 	ctx.Data["Title"] = "Branches"
-	ctx.Data["IsRepoToolbarBranches"] = true
 	ctx.Data["AllowsPulls"] = ctx.Repo.Repository.AllowsPulls(ctx)
 	ctx.Data["IsWriter"] = ctx.Repo.CanWrite(unit.TypeCode)
 	ctx.Data["IsMirror"] = ctx.Repo.Repository.IsMirror
diff --git a/routers/web/repo/code_frequency.go b/routers/web/repo/code_frequency.go
index 6572adce74..e212d3b60c 100644
--- a/routers/web/repo/code_frequency.go
+++ b/routers/web/repo/code_frequency.go
@@ -29,7 +29,7 @@ func CodeFrequency(ctx *context.Context) {
 
 // CodeFrequencyData returns JSON of code frequency data
 func CodeFrequencyData(ctx *context.Context) {
-	if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
+	if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil {
 		if errors.Is(err, contributors_service.ErrAwaitGeneration) {
 			ctx.Status(http.StatusAccepted)
 			return
diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go
index bad8ca56d1..8ffda8ae0a 100644
--- a/routers/web/repo/commit.go
+++ b/routers/web/repo/commit.go
@@ -62,11 +62,7 @@ func Commits(ctx *context.Context) {
 	}
 	ctx.Data["PageIsViewCode"] = true
 
-	commitsCount, err := ctx.Repo.GetCommitsCount()
-	if err != nil {
-		ctx.ServerError("GetCommitsCount", err)
-		return
-	}
+	commitsCount := ctx.Repo.CommitsCount
 
 	page := ctx.FormInt("page")
 	if page <= 1 {
@@ -129,12 +125,6 @@ func Graph(ctx *context.Context) {
 	ctx.Data["SelectedBranches"] = realBranches
 	files := ctx.FormStrings("file")
 
-	commitsCount, err := ctx.Repo.GetCommitsCount()
-	if err != nil {
-		ctx.ServerError("GetCommitsCount", err)
-		return
-	}
-
 	graphCommitsCount, err := ctx.Repo.GetCommitGraphsCount(ctx, hidePRRefs, realBranches, files)
 	if err != nil {
 		log.Warn("GetCommitGraphsCount error for generate graph exclude prs: %t branches: %s in %-v, Will Ignore branches and try again. Underlying Error: %v", hidePRRefs, branches, ctx.Repo.Repository, err)
@@ -171,7 +161,6 @@ func Graph(ctx *context.Context) {
 
 	ctx.Data["Username"] = ctx.Repo.Owner.Name
 	ctx.Data["Reponame"] = ctx.Repo.Repository.Name
-	ctx.Data["CommitCount"] = commitsCount
 
 	paginator := context.NewPagination(int(graphCommitsCount), setting.UI.GraphMaxCommitNum, page, 5)
 	paginator.AddParamFromRequest(ctx.Req)
diff --git a/routers/web/repo/contributors.go b/routers/web/repo/contributors.go
index e9c0919955..93dec1f350 100644
--- a/routers/web/repo/contributors.go
+++ b/routers/web/repo/contributors.go
@@ -26,7 +26,7 @@ func Contributors(ctx *context.Context) {
 
 // ContributorsData renders JSON of contributors along with their weekly commit statistics
 func ContributorsData(ctx *context.Context) {
-	if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
+	if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil {
 		if errors.Is(err, contributors_service.ErrAwaitGeneration) {
 			ctx.Status(http.StatusAccepted)
 			return
diff --git a/routers/web/repo/issue_dependency.go b/routers/web/repo/issue_dependency.go
index f1d133edb0..0f6787386d 100644
--- a/routers/web/repo/issue_dependency.go
+++ b/routers/web/repo/issue_dependency.go
@@ -109,7 +109,7 @@ func RemoveDependency(ctx *context.Context) {
 	}
 
 	// Dependency Type
-	depTypeStr := ctx.Req.PostForm.Get("dependencyType")
+	depTypeStr := ctx.Req.PostFormValue("dependencyType")
 
 	var depType issues_model.DependencyType
 
diff --git a/routers/web/repo/issue_label_test.go b/routers/web/repo/issue_label_test.go
index 8a613e2c7e..486c2e35a2 100644
--- a/routers/web/repo/issue_label_test.go
+++ b/routers/web/repo/issue_label_test.go
@@ -38,7 +38,7 @@ func TestInitializeLabels(t *testing.T) {
 	contexttest.LoadRepo(t, ctx, 2)
 	web.SetForm(ctx, &forms.InitializeLabelsForm{TemplateName: "Default"})
 	InitializeLabels(ctx)
-	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
 	unittest.AssertExistsAndLoadBean(t, &issues_model.Label{
 		RepoID: 2,
 		Name:   "enhancement",
@@ -84,7 +84,7 @@ func TestNewLabel(t *testing.T) {
 		Color: "#abcdef",
 	})
 	NewLabel(ctx)
-	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
 	unittest.AssertExistsAndLoadBean(t, &issues_model.Label{
 		Name:  "newlabel",
 		Color: "#abcdef",
@@ -104,7 +104,7 @@ func TestUpdateLabel(t *testing.T) {
 		IsArchived: true,
 	})
 	UpdateLabel(ctx)
-	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
 	unittest.AssertExistsAndLoadBean(t, &issues_model.Label{
 		ID:    2,
 		Name:  "newnameforlabel",
@@ -120,7 +120,7 @@ func TestDeleteLabel(t *testing.T) {
 	contexttest.LoadRepo(t, ctx, 1)
 	ctx.Req.Form.Set("id", "2")
 	DeleteLabel(ctx)
-	assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
 	unittest.AssertNotExistsBean(t, &issues_model.Label{ID: 2})
 	unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{LabelID: 2})
 	assert.EqualValues(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg)
@@ -134,7 +134,7 @@ func TestUpdateIssueLabel_Clear(t *testing.T) {
 	ctx.Req.Form.Set("issue_ids", "1,3")
 	ctx.Req.Form.Set("action", "clear")
 	UpdateIssueLabel(ctx)
-	assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
 	unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: 1})
 	unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: 3})
 	unittest.CheckConsistencyFor(t, &issues_model.Label{})
@@ -160,7 +160,7 @@ func TestUpdateIssueLabel_Toggle(t *testing.T) {
 		ctx.Req.Form.Set("action", testCase.Action)
 		ctx.Req.Form.Set("id", strconv.Itoa(int(testCase.LabelID)))
 		UpdateIssueLabel(ctx)
-		assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+		assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
 		for _, issueID := range testCase.IssueIDs {
 			if testCase.ExpectedAdd {
 				unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: testCase.LabelID})
diff --git a/routers/web/repo/issue_timetrack.go b/routers/web/repo/issue_timetrack.go
index 36e931a48f..134ded82d1 100644
--- a/routers/web/repo/issue_timetrack.go
+++ b/routers/web/repo/issue_timetrack.go
@@ -81,7 +81,7 @@ func DeleteTime(c *context.Context) {
 		return
 	}
 
-	c.Flash.Success(c.Tr("repo.issues.del_time_history", util.SecToTime(t.Time)))
+	c.Flash.Success(c.Tr("repo.issues.del_time_history", util.SecToHours(t.Time)))
 	c.JSONRedirect("")
 }
 
diff --git a/routers/web/repo/issue_watch.go b/routers/web/repo/issue_watch.go
index a2a4be1758..e7d55e5555 100644
--- a/routers/web/repo/issue_watch.go
+++ b/routers/web/repo/issue_watch.go
@@ -46,7 +46,7 @@ func IssueWatch(ctx *context.Context) {
 		return
 	}
 
-	watch, err := strconv.ParseBool(ctx.Req.PostForm.Get("watch"))
+	watch, err := strconv.ParseBool(ctx.Req.PostFormValue("watch"))
 	if err != nil {
 		ctx.ServerError("watch is not bool", err)
 		return
diff --git a/routers/web/repo/recent_commits.go b/routers/web/repo/recent_commits.go
index dc72081900..228eb0dbac 100644
--- a/routers/web/repo/recent_commits.go
+++ b/routers/web/repo/recent_commits.go
@@ -29,7 +29,7 @@ func RecentCommits(ctx *context.Context) {
 
 // RecentCommitsData returns JSON of recent commits data
 func RecentCommitsData(ctx *context.Context) {
-	if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
+	if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil {
 		if errors.Is(err, contributors_service.ErrAwaitGeneration) {
 			ctx.Status(http.StatusAccepted)
 			return
diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go
index d589586dad..bbbe5c1081 100644
--- a/routers/web/repo/search.go
+++ b/routers/web/repo/search.go
@@ -67,10 +67,11 @@ func Search(ctx *context.Context) {
 			ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx)
 		}
 	} else {
+		searchRefName := git.RefNameFromBranch(ctx.Repo.Repository.DefaultBranch) // BranchName should be default branch or the first existing branch
 		res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, prepareSearch.Keyword, git.GrepOptions{
 			ContextLineNumber: 1,
 			IsFuzzy:           prepareSearch.IsFuzzy,
-			RefName:           git.RefNameFromBranch(ctx.Repo.Repository.DefaultBranch).String(), // BranchName should be default branch or the first existing branch
+			RefName:           searchRefName.String(),
 			PathspecList:      indexSettingToGitGrepPathspecList(),
 		})
 		if err != nil {
@@ -78,6 +79,11 @@ func Search(ctx *context.Context) {
 			ctx.ServerError("GrepSearch", err)
 			return
 		}
+		commitID, err := ctx.Repo.GitRepo.GetRefCommitID(searchRefName.String())
+		if err != nil {
+			ctx.ServerError("GetRefCommitID", err)
+			return
+		}
 		total = len(res)
 		pageStart := min((page-1)*setting.UI.RepoSearchPagingNum, len(res))
 		pageEnd := min(page*setting.UI.RepoSearchPagingNum, len(res))
@@ -86,7 +92,7 @@ func Search(ctx *context.Context) {
 			searchResults = append(searchResults, &code_indexer.Result{
 				RepoID:   ctx.Repo.Repository.ID,
 				Filename: r.Filename,
-				CommitID: ctx.Repo.CommitID,
+				CommitID: commitID,
 				// UpdatedUnix: not supported yet
 				// Language:    not supported yet
 				// Color:       not supported yet
diff --git a/routers/web/repo/setting/protected_branch.go b/routers/web/repo/setting/protected_branch.go
index 022a24a9ad..06a9e69507 100644
--- a/routers/web/repo/setting/protected_branch.go
+++ b/routers/web/repo/setting/protected_branch.go
@@ -4,6 +4,7 @@
 package setting
 
 import (
+	"errors"
 	"fmt"
 	"net/http"
 	"net/url"
@@ -14,6 +15,7 @@ import (
 	"code.gitea.io/gitea/models/organization"
 	"code.gitea.io/gitea/models/perm"
 	access_model "code.gitea.io/gitea/models/perm/access"
+	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/templates"
 	"code.gitea.io/gitea/modules/web"
@@ -351,9 +353,15 @@ func RenameBranchPost(ctx *context.Context) {
 	msg, err := repository.RenameBranch(ctx, ctx.Repo.Repository, ctx.Doer, ctx.Repo.GitRepo, form.From, form.To)
 	if err != nil {
 		switch {
+		case repo_model.IsErrUserDoesNotHaveAccessToRepo(err):
+			ctx.Flash.Error(ctx.Tr("repo.branch.rename_default_or_protected_branch_error"))
+			ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
 		case git_model.IsErrBranchAlreadyExists(err):
 			ctx.Flash.Error(ctx.Tr("repo.branch.branch_already_exists", form.To))
 			ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
+		case errors.Is(err, git_model.ErrBranchIsProtected):
+			ctx.Flash.Error(ctx.Tr("repo.branch.rename_protected_branch_failed"))
+			ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
 		default:
 			ctx.ServerError("RenameBranch", err)
 		}
diff --git a/routers/web/repo/setting/settings_test.go b/routers/web/repo/setting/settings_test.go
index 09586cc68d..ad33dac514 100644
--- a/routers/web/repo/setting/settings_test.go
+++ b/routers/web/repo/setting/settings_test.go
@@ -54,7 +54,7 @@ func TestAddReadOnlyDeployKey(t *testing.T) {
 	}
 	web.SetForm(ctx, &addKeyForm)
 	DeployKeysPost(ctx)
-	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
 
 	unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{
 		Name:    addKeyForm.Title,
@@ -84,7 +84,7 @@ func TestAddReadWriteOnlyDeployKey(t *testing.T) {
 	}
 	web.SetForm(ctx, &addKeyForm)
 	DeployKeysPost(ctx)
-	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
 
 	unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{
 		Name:    addKeyForm.Title,
@@ -121,7 +121,7 @@ func TestCollaborationPost(t *testing.T) {
 
 	CollaborationPost(ctx)
 
-	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
 
 	exists, err := repo_model.IsCollaborator(ctx, re.ID, 4)
 	assert.NoError(t, err)
@@ -147,7 +147,7 @@ func TestCollaborationPost_InactiveUser(t *testing.T) {
 
 	CollaborationPost(ctx)
 
-	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
 	assert.NotEmpty(t, ctx.Flash.ErrorMsg)
 }
 
@@ -179,7 +179,7 @@ func TestCollaborationPost_AddCollaboratorTwice(t *testing.T) {
 
 	CollaborationPost(ctx)
 
-	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
 
 	exists, err := repo_model.IsCollaborator(ctx, re.ID, 4)
 	assert.NoError(t, err)
@@ -188,7 +188,7 @@ func TestCollaborationPost_AddCollaboratorTwice(t *testing.T) {
 	// Try adding the same collaborator again
 	CollaborationPost(ctx)
 
-	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
 	assert.NotEmpty(t, ctx.Flash.ErrorMsg)
 }
 
@@ -210,7 +210,7 @@ func TestCollaborationPost_NonExistentUser(t *testing.T) {
 
 	CollaborationPost(ctx)
 
-	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
 	assert.NotEmpty(t, ctx.Flash.ErrorMsg)
 }
 
@@ -250,7 +250,7 @@ func TestAddTeamPost(t *testing.T) {
 	AddTeamPost(ctx)
 
 	assert.True(t, repo_service.HasRepository(db.DefaultContext, team, re.ID))
-	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
 	assert.Empty(t, ctx.Flash.ErrorMsg)
 }
 
@@ -290,7 +290,7 @@ func TestAddTeamPost_NotAllowed(t *testing.T) {
 	AddTeamPost(ctx)
 
 	assert.False(t, repo_service.HasRepository(db.DefaultContext, team, re.ID))
-	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
 	assert.NotEmpty(t, ctx.Flash.ErrorMsg)
 }
 
@@ -331,7 +331,7 @@ func TestAddTeamPost_AddTeamTwice(t *testing.T) {
 
 	AddTeamPost(ctx)
 	assert.True(t, repo_service.HasRepository(db.DefaultContext, team, re.ID))
-	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
 	assert.NotEmpty(t, ctx.Flash.ErrorMsg)
 }
 
@@ -364,7 +364,7 @@ func TestAddTeamPost_NonExistentTeam(t *testing.T) {
 	ctx.Repo = repo
 
 	AddTeamPost(ctx)
-	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
 	assert.NotEmpty(t, ctx.Flash.ErrorMsg)
 }
 
diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go
index 1b0ba83af4..ce67ea3c01 100644
--- a/routers/web/repo/setting/webhook.go
+++ b/routers/web/repo/setting/webhook.go
@@ -654,6 +654,8 @@ func TestWebhook(ctx *context.Context) {
 	}
 
 	// Grab latest commit or fake one if it's empty repository.
+	// Note: in old code, the "ctx.Repo.Commit" is the last commit of the default branch.
+	// New code doesn't set that commit, so it always uses the fake commit to test webhook.
 	commit := ctx.Repo.Commit
 	if commit == nil {
 		ghost := user_model.NewGhostUser()
diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go
index ff2ee3c7ec..d141df181c 100644
--- a/routers/web/repo/view_home.go
+++ b/routers/web/repo/view_home.go
@@ -215,10 +215,28 @@ func prepareRecentlyPushedNewBranches(ctx *context.Context) {
 		if !opts.Repo.IsMirror && !opts.BaseRepo.IsMirror &&
 			opts.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests) &&
 			baseRepoPerm.CanRead(unit_model.TypePullRequests) {
-			ctx.Data["RecentlyPushedNewBranches"], err = git_model.FindRecentlyPushedNewBranches(ctx, ctx.Doer, opts)
+			var finalBranches []*git_model.RecentlyPushedNewBranch
+			branches, err := git_model.FindRecentlyPushedNewBranches(ctx, ctx.Doer, opts)
 			if err != nil {
 				log.Error("FindRecentlyPushedNewBranches failed: %v", err)
 			}
+
+			for _, branch := range branches {
+				divergingInfo, err := repo_service.GetBranchDivergingInfo(ctx,
+					branch.BranchRepo, branch.BranchName, // "base" repo for diverging info
+					opts.BaseRepo, opts.BaseRepo.DefaultBranch, // "head" repo for diverging info
+				)
+				if err != nil {
+					log.Error("GetBranchDivergingInfo failed: %v", err)
+					continue
+				}
+				branchRepoHasNewCommits := divergingInfo.BaseHasNewCommits
+				baseRepoCommitsBehind := divergingInfo.HeadCommitsBehind
+				if branchRepoHasNewCommits || baseRepoCommitsBehind > 0 {
+					finalBranches = append(finalBranches, branch)
+				}
+			}
+			ctx.Data["RecentlyPushedNewBranches"] = finalBranches
 		}
 	}
 }
diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go
index c31f29164b..99114c93e0 100644
--- a/routers/web/repo/wiki_test.go
+++ b/routers/web/repo/wiki_test.go
@@ -82,7 +82,7 @@ func TestWiki(t *testing.T) {
 	ctx.SetPathParam("*", "Home")
 	contexttest.LoadRepo(t, ctx, 1)
 	Wiki(ctx)
-	assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
 	assert.EqualValues(t, "Home", ctx.Data["Title"])
 	assertPagesMetas(t, []string{"Home", "Page With Image", "Page With Spaced Name", "Unescaped File"}, ctx.Data["Pages"])
 
@@ -90,7 +90,7 @@ func TestWiki(t *testing.T) {
 	ctx.SetPathParam("*", "jpeg.jpg")
 	contexttest.LoadRepo(t, ctx, 1)
 	Wiki(ctx)
-	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
 	assert.Equal(t, "/user2/repo1/wiki/raw/jpeg.jpg", ctx.Resp.Header().Get("Location"))
 }
 
@@ -100,7 +100,7 @@ func TestWikiPages(t *testing.T) {
 	ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki/?action=_pages")
 	contexttest.LoadRepo(t, ctx, 1)
 	WikiPages(ctx)
-	assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
 	assertPagesMetas(t, []string{"Home", "Page With Image", "Page With Spaced Name", "Unescaped File"}, ctx.Data["Pages"])
 }
 
@@ -111,7 +111,7 @@ func TestNewWiki(t *testing.T) {
 	contexttest.LoadUser(t, ctx, 2)
 	contexttest.LoadRepo(t, ctx, 1)
 	NewWiki(ctx)
-	assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
 	assert.EqualValues(t, ctx.Tr("repo.wiki.new_page"), ctx.Data["Title"])
 }
 
@@ -131,7 +131,7 @@ func TestNewWikiPost(t *testing.T) {
 			Message: message,
 		})
 		NewWikiPost(ctx)
-		assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+		assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
 		assertWikiExists(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title))
 		assert.Equal(t, content, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)))
 	}
@@ -149,7 +149,7 @@ func TestNewWikiPost_ReservedName(t *testing.T) {
 		Message: message,
 	})
 	NewWikiPost(ctx)
-	assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
 	assert.EqualValues(t, ctx.Tr("repo.wiki.reserved_page", "_edit"), ctx.Flash.ErrorMsg)
 	assertWikiNotExists(t, ctx.Repo.Repository, "_edit")
 }
@@ -162,7 +162,7 @@ func TestEditWiki(t *testing.T) {
 	contexttest.LoadUser(t, ctx, 2)
 	contexttest.LoadRepo(t, ctx, 1)
 	EditWiki(ctx)
-	assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
 	assert.EqualValues(t, "Home", ctx.Data["Title"])
 	assert.Equal(t, wikiContent(t, ctx.Repo.Repository, "Home"), ctx.Data["content"])
 
@@ -171,7 +171,7 @@ func TestEditWiki(t *testing.T) {
 	contexttest.LoadUser(t, ctx, 2)
 	contexttest.LoadRepo(t, ctx, 1)
 	EditWiki(ctx)
-	assert.EqualValues(t, http.StatusForbidden, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusForbidden, ctx.Resp.WrittenStatus())
 }
 
 func TestEditWikiPost(t *testing.T) {
@@ -190,7 +190,7 @@ func TestEditWikiPost(t *testing.T) {
 			Message: message,
 		})
 		EditWikiPost(ctx)
-		assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+		assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
 		assertWikiExists(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title))
 		assert.Equal(t, content, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)))
 		if title != "Home" {
@@ -206,7 +206,7 @@ func TestDeleteWikiPagePost(t *testing.T) {
 	contexttest.LoadUser(t, ctx, 2)
 	contexttest.LoadRepo(t, ctx, 1)
 	DeleteWikiPagePost(ctx)
-	assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
 	assertWikiNotExists(t, ctx.Repo.Repository, "Home")
 }
 
@@ -228,9 +228,9 @@ func TestWikiRaw(t *testing.T) {
 		contexttest.LoadRepo(t, ctx, 1)
 		WikiRaw(ctx)
 		if filetype == "" {
-			assert.EqualValues(t, http.StatusNotFound, ctx.Resp.Status(), "filepath: %s", filepath)
+			assert.EqualValues(t, http.StatusNotFound, ctx.Resp.WrittenStatus(), "filepath: %s", filepath)
 		} else {
-			assert.EqualValues(t, http.StatusOK, ctx.Resp.Status(), "filepath: %s", filepath)
+			assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus(), "filepath: %s", filepath)
 			assert.EqualValues(t, filetype, ctx.Resp.Header().Get("Content-Type"), "filepath: %s", filepath)
 		}
 	}
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index c79648a455..235a7c6f39 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -576,17 +576,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 	// -------------------------------
 	// Fill stats to post to ctx.Data.
 	// -------------------------------
-	issueStats, err := getUserIssueStats(ctx, filterMode, issue_indexer.ToSearchOptions(keyword, opts).Copy(
+	issueStats, err := getUserIssueStats(ctx, ctxUser, filterMode, issue_indexer.ToSearchOptions(keyword, opts).Copy(
 		func(o *issue_indexer.SearchOptions) {
 			o.IsFuzzyKeyword = isFuzzy
-			// If the doer is the same as the context user, which means the doer is viewing his own dashboard,
-			// it's not enough to show the repos that the doer owns or has been explicitly granted access to,
-			// because the doer may create issues or be mentioned in any public repo.
-			// So we need search issues in all public repos.
-			o.AllPublic = ctx.Doer.ID == ctxUser.ID
-			o.MentionID = nil
-			o.ReviewRequestedID = nil
-			o.ReviewedID = nil
 		},
 	))
 	if err != nil {
@@ -775,10 +767,19 @@ func UsernameSubRoute(ctx *context.Context) {
 	}
 }
 
-func getUserIssueStats(ctx *context.Context, filterMode int, opts *issue_indexer.SearchOptions) (ret *issues_model.IssueStats, err error) {
+func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMode int, opts *issue_indexer.SearchOptions) (ret *issues_model.IssueStats, err error) {
 	ret = &issues_model.IssueStats{}
 	doerID := ctx.Doer.ID
 
+	opts = opts.Copy(func(o *issue_indexer.SearchOptions) {
+		// If the doer is the same as the context user, which means the doer is viewing his own dashboard,
+		// it's not enough to show the repos that the doer owns or has been explicitly granted access to,
+		// because the doer may create issues or be mentioned in any public repo.
+		// So we need search issues in all public repos.
+		o.AllPublic = doerID == ctxUser.ID
+	})
+
+	// Open/Closed are for the tabs of the issue list
 	{
 		openClosedOpts := opts.Copy()
 		switch filterMode {
@@ -809,6 +810,15 @@ func getUserIssueStats(ctx *context.Context, filterMode int, opts *issue_indexer
 		}
 	}
 
+	// Below stats are for the left sidebar
+	opts = opts.Copy(func(o *issue_indexer.SearchOptions) {
+		o.AssigneeID = nil
+		o.PosterID = nil
+		o.MentionID = nil
+		o.ReviewRequestedID = nil
+		o.ReviewedID = nil
+	})
+
 	ret.YourRepositoriesCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AllPublic = false }))
 	if err != nil {
 		return nil, err
diff --git a/routers/web/user/home_test.go b/routers/web/user/home_test.go
index 51246551ea..b2c8ad98ba 100644
--- a/routers/web/user/home_test.go
+++ b/routers/web/user/home_test.go
@@ -45,7 +45,7 @@ func TestArchivedIssues(t *testing.T) {
 	Issues(ctx)
 
 	// Assert: One Issue (ID 30) from one Repo (ID 50) is retrieved, while nothing from archived Repo 51 is retrieved
-	assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
 
 	assert.Len(t, ctx.Data["Issues"], 1)
 }
@@ -58,7 +58,7 @@ func TestIssues(t *testing.T) {
 	contexttest.LoadUser(t, ctx, 2)
 	ctx.Req.Form.Set("state", "closed")
 	Issues(ctx)
-	assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
 
 	assert.EqualValues(t, true, ctx.Data["IsShowClosed"])
 	assert.Len(t, ctx.Data["Issues"], 1)
@@ -72,7 +72,7 @@ func TestPulls(t *testing.T) {
 	contexttest.LoadUser(t, ctx, 2)
 	ctx.Req.Form.Set("state", "open")
 	Pulls(ctx)
-	assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
 
 	assert.Len(t, ctx.Data["Issues"], 5)
 }
@@ -87,7 +87,7 @@ func TestMilestones(t *testing.T) {
 	ctx.Req.Form.Set("state", "closed")
 	ctx.Req.Form.Set("sort", "furthestduedate")
 	Milestones(ctx)
-	assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
 	assert.EqualValues(t, map[int64]int64{1: 1}, ctx.Data["Counts"])
 	assert.EqualValues(t, true, ctx.Data["IsShowClosed"])
 	assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"])
@@ -107,7 +107,7 @@ func TestMilestonesForSpecificRepo(t *testing.T) {
 	ctx.Req.Form.Set("state", "closed")
 	ctx.Req.Form.Set("sort", "furthestduedate")
 	Milestones(ctx)
-	assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+	assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
 	assert.EqualValues(t, map[int64]int64{1: 1}, ctx.Data["Counts"])
 	assert.EqualValues(t, true, ctx.Data["IsShowClosed"])
 	assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"])
diff --git a/routers/web/user/setting/account_test.go b/routers/web/user/setting/account_test.go
index 9fdc5e4d53..13caa33771 100644
--- a/routers/web/user/setting/account_test.go
+++ b/routers/web/user/setting/account_test.go
@@ -95,7 +95,7 @@ func TestChangePassword(t *testing.T) {
 			AccountPost(ctx)
 
 			assert.Contains(t, ctx.Flash.ErrorMsg, req.Message)
-			assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+			assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
 		})
 	}
 }
diff --git a/routers/web/web.go b/routers/web/web.go
index f5e21a7070..02a3d39670 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -720,6 +720,7 @@ func registerRoutes(m *web.Router) {
 		m.Group("/monitor", func() {
 			m.Get("/stats", admin.MonitorStats)
 			m.Get("/cron", admin.CronTasks)
+			m.Get("/perftrace", admin.PerfTrace)
 			m.Get("/stacktrace", admin.Stacktrace)
 			m.Post("/stacktrace/cancel/{pid}", admin.StacktraceCancel)
 			m.Get("/queue", admin.Queues)
@@ -1156,7 +1157,7 @@ func registerRoutes(m *web.Router) {
 			m.Post("/cancel", repo.MigrateCancelPost)
 		})
 	},
-		reqSignIn, context.RepoAssignment, reqRepoAdmin, context.RepoRef(),
+		reqSignIn, context.RepoAssignment, reqRepoAdmin,
 		ctxDataSet("PageIsRepoSettings", true, "LFSStartServer", setting.LFS.StartServer),
 	)
 	// end "/{username}/{reponame}/settings"
@@ -1342,7 +1343,7 @@ func registerRoutes(m *web.Router) {
 
 	m.Group("/{username}/{reponame}", func() { // repo tags
 		m.Group("/tags", func() {
-			m.Get("", repo.TagsList)
+			m.Get("", context.RepoRefByDefaultBranch() /* for the "commits" tab */, repo.TagsList)
 			m.Get(".rss", feedEnabled, repo.TagsListFeedRSS)
 			m.Get(".atom", feedEnabled, repo.TagsListFeedAtom)
 			m.Get("/list", repo.GetTagList)
@@ -1523,7 +1524,7 @@ func registerRoutes(m *web.Router) {
 		m.Group("/activity_author_data", func() {
 			m.Get("", repo.ActivityAuthors)
 			m.Get("/{period}", repo.ActivityAuthors)
-		}, context.RepoRef(), repo.MustBeNotEmpty)
+		}, repo.MustBeNotEmpty)
 
 		m.Group("/archive", func() {
 			m.Get("/*", repo.Download)
@@ -1532,8 +1533,8 @@ func registerRoutes(m *web.Router) {
 
 		m.Group("/branches", func() {
 			m.Get("/list", repo.GetBranchesList)
-			m.Get("", repo.Branches)
-		}, repo.MustBeNotEmpty, context.RepoRef())
+			m.Get("", context.RepoRefByDefaultBranch() /* for the "commits" tab */, repo.Branches)
+		}, repo.MustBeNotEmpty)
 
 		m.Group("/media", func() {
 			m.Get("/blob/{sha}", repo.DownloadByIDOrLFS)
@@ -1577,8 +1578,10 @@ func registerRoutes(m *web.Router) {
 			m.Get("/graph", repo.Graph)
 			m.Get("/commit/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
 			m.Get("/commit/{sha:([a-f0-9]{7,64})$}/load-branches-and-tags", repo.LoadBranchesAndTags)
-			m.Get("/cherry-pick/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, repo.CherryPick)
-		}, repo.MustBeNotEmpty, context.RepoRef())
+
+			// FIXME: this route `/cherry-pick/{sha}` doesn't seem useful or right, the new code always uses `/_cherrypick/` which could handle branch name correctly
+			m.Get("/cherry-pick/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, context.RepoRefByDefaultBranch(), repo.CherryPick)
+		}, repo.MustBeNotEmpty)
 
 		m.Get("/rss/branch/*", context.RepoRefByType(git.RefTypeBranch), feedEnabled, feed.RenderBranchFeed)
 		m.Get("/atom/branch/*", context.RepoRefByType(git.RefTypeBranch), feedEnabled, feed.RenderBranchFeed)
@@ -1632,7 +1635,7 @@ func registerRoutes(m *web.Router) {
 
 	m.NotFound(func(w http.ResponseWriter, req *http.Request) {
 		ctx := context.GetWebContext(req)
-		routing.UpdateFuncInfo(ctx, routing.GetFuncInfo(ctx.NotFound, "WebNotFound"))
+		defer routing.RecordFuncInfo(ctx, routing.GetFuncInfo(ctx.NotFound, "WebNotFound"))()
 		ctx.NotFound("", nil)
 	})
 }
diff --git a/services/context/access_log.go b/services/context/access_log.go
index 0926748ac5..001d93a362 100644
--- a/services/context/access_log.go
+++ b/services/context/access_log.go
@@ -18,13 +18,14 @@ import (
 	"code.gitea.io/gitea/modules/web/middleware"
 )
 
-type routerLoggerOptions struct {
-	req            *http.Request
+type accessLoggerTmplData struct {
 	Identity       *string
 	Start          *time.Time
-	ResponseWriter http.ResponseWriter
-	Ctx            map[string]any
-	RequestID      *string
+	ResponseWriter struct {
+		Status, Size int
+	}
+	Ctx       map[string]any
+	RequestID *string
 }
 
 const keyOfRequestIDInTemplate = ".RequestID"
@@ -51,51 +52,65 @@ func parseRequestIDFromRequestHeader(req *http.Request) string {
 	return requestID
 }
 
+type accessLogRecorder struct {
+	logger        log.BaseLogger
+	logTemplate   *template.Template
+	needRequestID bool
+}
+
+func (lr *accessLogRecorder) record(start time.Time, respWriter ResponseWriter, req *http.Request) {
+	var requestID string
+	if lr.needRequestID {
+		requestID = parseRequestIDFromRequestHeader(req)
+	}
+
+	reqHost, _, err := net.SplitHostPort(req.RemoteAddr)
+	if err != nil {
+		reqHost = req.RemoteAddr
+	}
+
+	identity := "-"
+	data := middleware.GetContextData(req.Context())
+	if signedUser, ok := data[middleware.ContextDataKeySignedUser].(*user_model.User); ok {
+		identity = signedUser.Name
+	}
+	buf := bytes.NewBuffer([]byte{})
+	tmplData := accessLoggerTmplData{
+		Identity: &identity,
+		Start:    &start,
+		Ctx: map[string]any{
+			"RemoteAddr": req.RemoteAddr,
+			"RemoteHost": reqHost,
+			"Req":        req,
+		},
+		RequestID: &requestID,
+	}
+	tmplData.ResponseWriter.Status = respWriter.WrittenStatus()
+	tmplData.ResponseWriter.Size = respWriter.WrittenSize()
+	err = lr.logTemplate.Execute(buf, tmplData)
+	if err != nil {
+		log.Error("Could not execute access logger template: %v", err.Error())
+	}
+
+	lr.logger.Log(1, log.INFO, "%s", buf.String())
+}
+
+func newAccessLogRecorder() *accessLogRecorder {
+	return &accessLogRecorder{
+		logger:        log.GetLogger("access"),
+		logTemplate:   template.Must(template.New("log").Parse(setting.Log.AccessLogTemplate)),
+		needRequestID: len(setting.Log.RequestIDHeaders) > 0 && strings.Contains(setting.Log.AccessLogTemplate, keyOfRequestIDInTemplate),
+	}
+}
+
 // AccessLogger returns a middleware to log access logger
 func AccessLogger() func(http.Handler) http.Handler {
-	logger := log.GetLogger("access")
-	needRequestID := len(setting.Log.RequestIDHeaders) > 0 && strings.Contains(setting.Log.AccessLogTemplate, keyOfRequestIDInTemplate)
-	logTemplate, _ := template.New("log").Parse(setting.Log.AccessLogTemplate)
+	recorder := newAccessLogRecorder()
 	return func(next http.Handler) http.Handler {
 		return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
 			start := time.Now()
-
-			var requestID string
-			if needRequestID {
-				requestID = parseRequestIDFromRequestHeader(req)
-			}
-
-			reqHost, _, err := net.SplitHostPort(req.RemoteAddr)
-			if err != nil {
-				reqHost = req.RemoteAddr
-			}
-
 			next.ServeHTTP(w, req)
-			rw := w.(ResponseWriter)
-
-			identity := "-"
-			data := middleware.GetContextData(req.Context())
-			if signedUser, ok := data[middleware.ContextDataKeySignedUser].(*user_model.User); ok {
-				identity = signedUser.Name
-			}
-			buf := bytes.NewBuffer([]byte{})
-			err = logTemplate.Execute(buf, routerLoggerOptions{
-				req:            req,
-				Identity:       &identity,
-				Start:          &start,
-				ResponseWriter: rw,
-				Ctx: map[string]any{
-					"RemoteAddr": req.RemoteAddr,
-					"RemoteHost": reqHost,
-					"Req":        req,
-				},
-				RequestID: &requestID,
-			})
-			if err != nil {
-				log.Error("Could not execute access logger template: %v", err.Error())
-			}
-
-			logger.Info("%s", buf.String())
+			recorder.record(start, w.(ResponseWriter), req)
 		})
 	}
 }
diff --git a/services/context/access_log_test.go b/services/context/access_log_test.go
new file mode 100644
index 0000000000..bd3e47e0cc
--- /dev/null
+++ b/services/context/access_log_test.go
@@ -0,0 +1,71 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package context
+
+import (
+	"fmt"
+	"net/http"
+	"net/url"
+	"testing"
+	"time"
+
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
+
+	"github.com/stretchr/testify/assert"
+)
+
+type testAccessLoggerMock struct {
+	logs []string
+}
+
+func (t *testAccessLoggerMock) Log(skip int, level log.Level, format string, v ...any) {
+	t.logs = append(t.logs, fmt.Sprintf(format, v...))
+}
+
+func (t *testAccessLoggerMock) GetLevel() log.Level {
+	return log.INFO
+}
+
+type testAccessLoggerResponseWriterMock struct{}
+
+func (t testAccessLoggerResponseWriterMock) Header() http.Header {
+	return nil
+}
+
+func (t testAccessLoggerResponseWriterMock) Before(f func(ResponseWriter)) {}
+
+func (t testAccessLoggerResponseWriterMock) WriteHeader(statusCode int) {}
+
+func (t testAccessLoggerResponseWriterMock) Write(bytes []byte) (int, error) {
+	return 0, nil
+}
+
+func (t testAccessLoggerResponseWriterMock) Flush() {}
+
+func (t testAccessLoggerResponseWriterMock) WrittenStatus() int {
+	return http.StatusOK
+}
+
+func (t testAccessLoggerResponseWriterMock) WrittenSize() int {
+	return 123123
+}
+
+func TestAccessLogger(t *testing.T) {
+	setting.Log.AccessLogTemplate = `{{.Ctx.RemoteHost}} - {{.Identity}} {{.Start.Format "[02/Jan/2006:15:04:05 -0700]" }} "{{.Ctx.Req.Method}} {{.Ctx.Req.URL.RequestURI}} {{.Ctx.Req.Proto}}" {{.ResponseWriter.Status}} {{.ResponseWriter.Size}} "{{.Ctx.Req.Referer}}" "{{.Ctx.Req.UserAgent}}"`
+	recorder := newAccessLogRecorder()
+	mockLogger := &testAccessLoggerMock{}
+	recorder.logger = mockLogger
+	req := &http.Request{
+		RemoteAddr: "remote-addr",
+		Method:     "GET",
+		Proto:      "https",
+		URL:        &url.URL{Path: "/path"},
+	}
+	req.Header = http.Header{}
+	req.Header.Add("Referer", "referer")
+	req.Header.Add("User-Agent", "user-agent")
+	recorder.record(time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC), &testAccessLoggerResponseWriterMock{}, req)
+	assert.Equal(t, []string{`remote-addr - - [02/Jan/2000:03:04:05 +0000] "GET /path https" 200 123123 "referer" "user-agent"`}, mockLogger.logs)
+}
diff --git a/services/context/base.go b/services/context/base.go
index 7a39353e09..5db84f42a5 100644
--- a/services/context/base.go
+++ b/services/context/base.go
@@ -4,7 +4,6 @@
 package context
 
 import (
-	"context"
 	"fmt"
 	"html/template"
 	"io"
@@ -25,8 +24,7 @@ type BaseContextKeyType struct{}
 var BaseContextKey BaseContextKeyType
 
 type Base struct {
-	context.Context
-	reqctx.RequestDataStore
+	reqctx.RequestContext
 
 	Resp ResponseWriter
 	Req  *http.Request
@@ -172,19 +170,19 @@ func (b *Base) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
 }
 
 func NewBaseContext(resp http.ResponseWriter, req *http.Request) *Base {
-	ds := reqctx.GetRequestDataStore(req.Context())
+	reqCtx := reqctx.FromContext(req.Context())
 	b := &Base{
-		Context:          req.Context(),
-		RequestDataStore: ds,
-		Req:              req,
-		Resp:             WrapResponseWriter(resp),
-		Locale:           middleware.Locale(resp, req),
-		Data:             ds.GetData(),
+		RequestContext: reqCtx,
+
+		Req:    req,
+		Resp:   WrapResponseWriter(resp),
+		Locale: middleware.Locale(resp, req),
+		Data:   reqCtx.GetData(),
 	}
 	b.Req = b.Req.WithContext(b)
-	ds.SetContextValue(BaseContextKey, b)
-	ds.SetContextValue(translation.ContextKey, b.Locale)
-	ds.SetContextValue(httplib.RequestContextKey, b.Req)
+	reqCtx.SetContextValue(BaseContextKey, b)
+	reqCtx.SetContextValue(translation.ContextKey, b.Locale)
+	reqCtx.SetContextValue(httplib.RequestContextKey, b.Req)
 	return b
 }
 
diff --git a/services/context/repo.go b/services/context/repo.go
index ef54b9cee8..6cd70d139b 100644
--- a/services/context/repo.go
+++ b/services/context/repo.go
@@ -777,6 +777,18 @@ func repoRefFullName(typ git.RefType, shortName string) git.RefName {
 	}
 }
 
+func RepoRefByDefaultBranch() func(*Context) {
+	return func(ctx *Context) {
+		ctx.Repo.RefFullName = git.RefNameFromBranch(ctx.Repo.Repository.DefaultBranch)
+		ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch
+		ctx.Repo.Commit, _ = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName)
+		ctx.Repo.CommitsCount, _ = ctx.Repo.GetCommitsCount()
+		ctx.Data["RefFullName"] = ctx.Repo.RefFullName
+		ctx.Data["BranchName"] = ctx.Repo.BranchName
+		ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount
+	}
+}
+
 // RepoRefByType handles repository reference name for a specific type
 // of repository reference
 func RepoRefByType(detectRefType git.RefType) func(*Context) {
diff --git a/services/context/response.go b/services/context/response.go
index 2f271f211b..c7368ebc6f 100644
--- a/services/context/response.go
+++ b/services/context/response.go
@@ -11,31 +11,29 @@ import (
 
 // ResponseWriter represents a response writer for HTTP
 type ResponseWriter interface {
-	http.ResponseWriter
-	http.Flusher
-	web_types.ResponseStatusProvider
+	http.ResponseWriter              // provides Header/Write/WriteHeader
+	http.Flusher                     // provides Flush
+	web_types.ResponseStatusProvider // provides WrittenStatus
 
-	Before(func(ResponseWriter))
-
-	Status() int // used by access logger template
-	Size() int   // used by access logger template
+	Before(fn func(ResponseWriter))
+	WrittenSize() int
 }
 
-var _ ResponseWriter = &Response{}
+var _ ResponseWriter = (*Response)(nil)
 
 // Response represents a response
 type Response struct {
 	http.ResponseWriter
 	written        int
 	status         int
-	befores        []func(ResponseWriter)
+	beforeFuncs    []func(ResponseWriter)
 	beforeExecuted bool
 }
 
 // Write writes bytes to HTTP endpoint
 func (r *Response) Write(bs []byte) (int, error) {
 	if !r.beforeExecuted {
-		for _, before := range r.befores {
+		for _, before := range r.beforeFuncs {
 			before(r)
 		}
 		r.beforeExecuted = true
@@ -51,18 +49,14 @@ func (r *Response) Write(bs []byte) (int, error) {
 	return size, nil
 }
 
-func (r *Response) Status() int {
-	return r.status
-}
-
-func (r *Response) Size() int {
+func (r *Response) WrittenSize() int {
 	return r.written
 }
 
 // WriteHeader write status code
 func (r *Response) WriteHeader(statusCode int) {
 	if !r.beforeExecuted {
-		for _, before := range r.befores {
+		for _, before := range r.beforeFuncs {
 			before(r)
 		}
 		r.beforeExecuted = true
@@ -87,17 +81,13 @@ func (r *Response) WrittenStatus() int {
 
 // Before allows for a function to be called before the ResponseWriter has been written to. This is
 // useful for setting headers or any other operations that must happen before a response has been written.
-func (r *Response) Before(f func(ResponseWriter)) {
-	r.befores = append(r.befores, f)
+func (r *Response) Before(fn func(ResponseWriter)) {
+	r.beforeFuncs = append(r.beforeFuncs, fn)
 }
 
 func WrapResponseWriter(resp http.ResponseWriter) *Response {
 	if v, ok := resp.(*Response); ok {
 		return v
 	}
-	return &Response{
-		ResponseWriter: resp,
-		status:         0,
-		befores:        make([]func(ResponseWriter), 0),
-	}
+	return &Response{ResponseWriter: resp}
 }
diff --git a/services/convert/issue.go b/services/convert/issue.go
index e3124efd64..37935accca 100644
--- a/services/convert/issue.go
+++ b/services/convert/issue.go
@@ -16,6 +16,7 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/util"
 )
 
 func ToIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) *api.Issue {
@@ -186,7 +187,7 @@ func ToStopWatches(ctx context.Context, sws []*issues_model.Stopwatch) (api.Stop
 		result = append(result, api.StopWatch{
 			Created:       sw.CreatedUnix.AsTime(),
 			Seconds:       sw.Seconds(),
-			Duration:      sw.Duration(),
+			Duration:      util.SecToHours(sw.Seconds()),
 			IssueIndex:    issue.Index,
 			IssueTitle:    issue.Title,
 			RepoOwnerName: repo.OwnerName,
diff --git a/services/convert/issue_comment.go b/services/convert/issue_comment.go
index b8527ae233..9ad584a62f 100644
--- a/services/convert/issue_comment.go
+++ b/services/convert/issue_comment.go
@@ -74,7 +74,7 @@ func ToTimelineComment(ctx context.Context, repo *repo_model.Repository, c *issu
 			c.Content[0] == '|' {
 			// TimeTracking Comments from v1.21 on store the seconds instead of an formatted string
 			// so we check for the "|" delimiter and convert new to legacy format on demand
-			c.Content = util.SecToTime(c.Content[1:])
+			c.Content = util.SecToHours(c.Content[1:])
 		}
 
 		if c.Type == issues_model.CommentTypeChangeTimeEstimate {
diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go
index f42686bb71..f046e59678 100644
--- a/services/gitdiff/gitdiff.go
+++ b/services/gitdiff/gitdiff.go
@@ -1136,7 +1136,10 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi
 	} else {
 		actualBeforeCommitID := opts.BeforeCommitID
 		if len(actualBeforeCommitID) == 0 {
-			parentCommit, _ := commit.Parent(0)
+			parentCommit, err := commit.Parent(0)
+			if err != nil {
+				return nil, err
+			}
 			actualBeforeCommitID = parentCommit.ID.String()
 		}
 
@@ -1145,7 +1148,6 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi
 			AddDynamicArguments(actualBeforeCommitID, opts.AfterCommitID)
 		opts.BeforeCommitID = actualBeforeCommitID
 
-		var err error
 		beforeCommit, err = gitRepo.GetCommit(opts.BeforeCommitID)
 		if err != nil {
 			return nil, err
diff --git a/services/repository/branch.go b/services/repository/branch.go
index 302cfff62e..c80d367bbd 100644
--- a/services/repository/branch.go
+++ b/services/repository/branch.go
@@ -26,6 +26,7 @@ import (
 	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/queue"
 	repo_module "code.gitea.io/gitea/modules/repository"
+	"code.gitea.io/gitea/modules/reqctx"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
@@ -416,6 +417,29 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, doer *user_m
 		return "from_not_exist", nil
 	}
 
+	perm, err := access_model.GetUserRepoPermission(ctx, repo, doer)
+	if err != nil {
+		return "", err
+	}
+
+	isDefault := from == repo.DefaultBranch
+	if isDefault && !perm.IsAdmin() {
+		return "", repo_model.ErrUserDoesNotHaveAccessToRepo{
+			UserID:   doer.ID,
+			RepoName: repo.LowerName,
+		}
+	}
+
+	// If from == rule name, admins are allowed to modify them.
+	if protectedBranch, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, from); err != nil {
+		return "", err
+	} else if protectedBranch != nil && !perm.IsAdmin() {
+		return "", repo_model.ErrUserDoesNotHaveAccessToRepo{
+			UserID:   doer.ID,
+			RepoName: repo.LowerName,
+		}
+	}
+
 	if err := git_model.RenameBranch(ctx, repo, from, to, func(ctx context.Context, isDefault bool) error {
 		err2 := gitRepo.RenameBranch(from, to)
 		if err2 != nil {
@@ -642,3 +666,72 @@ func SetRepoDefaultBranch(ctx context.Context, repo *repo_model.Repository, gitR
 
 	return nil
 }
+
+// BranchDivergingInfo contains the information about the divergence of a head branch to the base branch.
+type BranchDivergingInfo struct {
+	// whether the base branch contains new commits which are not in the head branch
+	BaseHasNewCommits bool
+
+	// behind/after are number of commits that the head branch is behind/after the base branch, it's 0 if it's unable to calculate.
+	// there could be a case that BaseHasNewCommits=true while the behind/after are both 0 (unable to calculate).
+	HeadCommitsBehind int
+	HeadCommitsAhead  int
+}
+
+// GetBranchDivergingInfo returns the information about the divergence of a patch branch to the base branch.
+func GetBranchDivergingInfo(ctx reqctx.RequestContext, baseRepo *repo_model.Repository, baseBranch string, headRepo *repo_model.Repository, headBranch string) (*BranchDivergingInfo, error) {
+	headGitBranch, err := git_model.GetBranch(ctx, headRepo.ID, headBranch)
+	if err != nil {
+		return nil, err
+	}
+	if headGitBranch.IsDeleted {
+		return nil, git_model.ErrBranchNotExist{
+			BranchName: headBranch,
+		}
+	}
+	baseGitBranch, err := git_model.GetBranch(ctx, baseRepo.ID, baseBranch)
+	if err != nil {
+		return nil, err
+	}
+	if baseGitBranch.IsDeleted {
+		return nil, git_model.ErrBranchNotExist{
+			BranchName: baseBranch,
+		}
+	}
+
+	info := &BranchDivergingInfo{}
+	if headGitBranch.CommitID == baseGitBranch.CommitID {
+		return info, nil
+	}
+
+	// if the fork repo has new commits, this call will fail because they are not in the base repo
+	// exit status 128 - fatal: Invalid symmetric difference expression aaaaaaaaaaaa...bbbbbbbbbbbb
+	// so at the moment, we first check the update time, then check whether the fork branch has base's head
+	diff, err := git.GetDivergingCommits(ctx, baseRepo.RepoPath(), baseGitBranch.CommitID, headGitBranch.CommitID)
+	if err != nil {
+		info.BaseHasNewCommits = baseGitBranch.UpdatedUnix > headGitBranch.UpdatedUnix
+		if headRepo.IsFork && info.BaseHasNewCommits {
+			return info, nil
+		}
+		// if the base's update time is before the fork, check whether the base's head is in the fork
+		headGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, headRepo)
+		if err != nil {
+			return nil, err
+		}
+		headCommit, err := headGitRepo.GetCommit(headGitBranch.CommitID)
+		if err != nil {
+			return nil, err
+		}
+		baseCommitID, err := git.NewIDFromString(baseGitBranch.CommitID)
+		if err != nil {
+			return nil, err
+		}
+		hasPreviousCommit, _ := headCommit.HasPreviousCommit(baseCommitID)
+		info.BaseHasNewCommits = !hasPreviousCommit
+		return info, nil
+	}
+
+	info.HeadCommitsBehind, info.HeadCommitsAhead = diff.Behind, diff.Ahead
+	info.BaseHasNewCommits = info.HeadCommitsBehind > 0
+	return info, nil
+}
diff --git a/services/repository/merge_upstream.go b/services/repository/merge_upstream.go
index ef161889c0..34e01df723 100644
--- a/services/repository/merge_upstream.go
+++ b/services/repository/merge_upstream.go
@@ -4,38 +4,38 @@
 package repository
 
 import (
-	"context"
+	"errors"
 	"fmt"
 
-	git_model "code.gitea.io/gitea/models/git"
 	issue_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
-	"code.gitea.io/gitea/modules/gitrepo"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/reqctx"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/services/pull"
 )
 
-type UpstreamDivergingInfo struct {
-	BaseHasNewCommits bool
-	CommitsBehind     int
-	CommitsAhead      int
-}
-
 // MergeUpstream merges the base repository's default branch into the fork repository's current branch.
-func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, branch string) (mergeStyle string, err error) {
+func MergeUpstream(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_model.Repository, branch string) (mergeStyle string, err error) {
 	if err = repo.MustNotBeArchived(); err != nil {
 		return "", err
 	}
 	if err = repo.GetBaseRepo(ctx); err != nil {
 		return "", err
 	}
+	divergingInfo, err := GetUpstreamDivergingInfo(ctx, repo, branch)
+	if err != nil {
+		return "", err
+	}
+	if !divergingInfo.BaseBranchHasNewCommits {
+		return "up-to-date", nil
+	}
+
 	err = git.Push(ctx, repo.BaseRepo.RepoPath(), git.PushOptions{
 		Remote: repo.RepoPath(),
-		Branch: fmt.Sprintf("%s:%s", repo.BaseRepo.DefaultBranch, branch),
+		Branch: fmt.Sprintf("%s:%s", divergingInfo.BaseBranchName, branch),
 		Env:    repo_module.PushingEnvironment(doer, repo),
 	})
 	if err == nil {
@@ -67,7 +67,7 @@ func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model.
 		BaseRepoID: repo.BaseRepo.ID,
 		BaseRepo:   repo.BaseRepo,
 		HeadBranch: branch, // maybe HeadCommitID is not needed
-		BaseBranch: repo.BaseRepo.DefaultBranch,
+		BaseBranch: divergingInfo.BaseBranchName,
 	}
 	fakeIssue.PullRequest = fakePR
 	err = pull.Update(ctx, fakePR, doer, "merge upstream", false)
@@ -77,68 +77,47 @@ func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model.
 	return "merge", nil
 }
 
+// UpstreamDivergingInfo is also used in templates, so it needs to search for all references before changing it.
+type UpstreamDivergingInfo struct {
+	BaseBranchName          string
+	BaseBranchHasNewCommits bool
+	HeadBranchCommitsBehind int
+}
+
 // GetUpstreamDivergingInfo returns the information about the divergence between the fork repository's branch and the base repository's default branch.
-func GetUpstreamDivergingInfo(ctx reqctx.RequestContext, repo *repo_model.Repository, branch string) (*UpstreamDivergingInfo, error) {
-	if !repo.IsFork {
+func GetUpstreamDivergingInfo(ctx reqctx.RequestContext, forkRepo *repo_model.Repository, forkBranch string) (*UpstreamDivergingInfo, error) {
+	if !forkRepo.IsFork {
 		return nil, util.NewInvalidArgumentErrorf("repo is not a fork")
 	}
 
-	if repo.IsArchived {
+	if forkRepo.IsArchived {
 		return nil, util.NewInvalidArgumentErrorf("repo is archived")
 	}
 
-	if err := repo.GetBaseRepo(ctx); err != nil {
+	if err := forkRepo.GetBaseRepo(ctx); err != nil {
 		return nil, err
 	}
 
-	forkBranch, err := git_model.GetBranch(ctx, repo.ID, branch)
-	if err != nil {
-		return nil, err
+	// Do the best to follow the GitHub's behavior, suppose there is a `branch-a` in fork repo:
+	// * if `branch-a` exists in base repo: try to sync `base:branch-a` to `fork:branch-a`
+	// * if `branch-a` doesn't exist in base repo: try to sync `base:main` to `fork:branch-a`
+	info, err := GetBranchDivergingInfo(ctx, forkRepo.BaseRepo, forkBranch, forkRepo, forkBranch)
+	if err == nil {
+		return &UpstreamDivergingInfo{
+			BaseBranchName:          forkBranch,
+			BaseBranchHasNewCommits: info.BaseHasNewCommits,
+			HeadBranchCommitsBehind: info.HeadCommitsBehind,
+		}, nil
 	}
-
-	baseBranch, err := git_model.GetBranch(ctx, repo.BaseRepo.ID, repo.BaseRepo.DefaultBranch)
-	if err != nil {
-		return nil, err
+	if errors.Is(err, util.ErrNotExist) {
+		info, err = GetBranchDivergingInfo(ctx, forkRepo.BaseRepo, forkRepo.BaseRepo.DefaultBranch, forkRepo, forkBranch)
+		if err == nil {
+			return &UpstreamDivergingInfo{
+				BaseBranchName:          forkRepo.BaseRepo.DefaultBranch,
+				BaseBranchHasNewCommits: info.BaseHasNewCommits,
+				HeadBranchCommitsBehind: info.HeadCommitsBehind,
+			}, nil
+		}
 	}
-
-	info := &UpstreamDivergingInfo{}
-	if forkBranch.CommitID == baseBranch.CommitID {
-		return info, nil
-	}
-
-	// if the fork repo has new commits, this call will fail because they are not in the base repo
-	// exit status 128 - fatal: Invalid symmetric difference expression aaaaaaaaaaaa...bbbbbbbbbbbb
-	// so at the moment, we first check the update time, then check whether the fork branch has base's head
-	diff, err := git.GetDivergingCommits(ctx, repo.BaseRepo.RepoPath(), baseBranch.CommitID, forkBranch.CommitID)
-	if err != nil {
-		info.BaseHasNewCommits = baseBranch.UpdatedUnix > forkBranch.UpdatedUnix
-		if info.BaseHasNewCommits {
-			return info, nil
-		}
-
-		// if the base's update time is before the fork, check whether the base's head is in the fork
-		baseGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo.BaseRepo)
-		if err != nil {
-			return nil, err
-		}
-		headGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo)
-		if err != nil {
-			return nil, err
-		}
-
-		baseCommitID, err := baseGitRepo.ConvertToGitID(baseBranch.CommitID)
-		if err != nil {
-			return nil, err
-		}
-		headCommit, err := headGitRepo.GetCommit(forkBranch.CommitID)
-		if err != nil {
-			return nil, err
-		}
-		hasPreviousCommit, _ := headCommit.HasPreviousCommit(baseCommitID)
-		info.BaseHasNewCommits = !hasPreviousCommit
-		return info, nil
-	}
-
-	info.CommitsBehind, info.CommitsAhead = diff.Behind, diff.Ahead
-	return info, nil
+	return nil, err
 }
diff --git a/templates/admin/navbar.tmpl b/templates/admin/navbar.tmpl
index 2d8d20691b..ce3048ed9f 100644
--- a/templates/admin/navbar.tmpl
+++ b/templates/admin/navbar.tmpl
@@ -98,7 +98,7 @@
 		<a class="{{if .PageIsAdminNotices}}active {{end}}item" href="{{AppSubUrl}}/-/admin/notices">
 			{{ctx.Locale.Tr "admin.notices"}}
 		</a>
-		<details class="item toggleable-item" {{if or .PageIsAdminMonitorStats .PageIsAdminMonitorCron .PageIsAdminMonitorQueue .PageIsAdminMonitorStacktrace}}open{{end}}>
+		<details class="item toggleable-item" {{if or .PageIsAdminMonitorStats .PageIsAdminMonitorCron .PageIsAdminMonitorQueue .PageIsAdminMonitorTrace}}open{{end}}>
 			<summary>{{ctx.Locale.Tr "admin.monitor"}}</summary>
 			<div class="menu">
 				<a class="{{if .PageIsAdminMonitorStats}}active {{end}}item" href="{{AppSubUrl}}/-/admin/monitor/stats">
@@ -110,8 +110,8 @@
 				<a class="{{if .PageIsAdminMonitorQueue}}active {{end}}item" href="{{AppSubUrl}}/-/admin/monitor/queue">
 					{{ctx.Locale.Tr "admin.monitor.queues"}}
 				</a>
-				<a class="{{if .PageIsAdminMonitorStacktrace}}active {{end}}item" href="{{AppSubUrl}}/-/admin/monitor/stacktrace">
-					{{ctx.Locale.Tr "admin.monitor.stacktrace"}}
+				<a class="{{if .PageIsAdminMonitorTrace}}active {{end}}item" href="{{AppSubUrl}}/-/admin/monitor/stacktrace">
+					{{ctx.Locale.Tr "admin.monitor.trace"}}
 				</a>
 			</div>
 		</details>
diff --git a/templates/admin/perftrace.tmpl b/templates/admin/perftrace.tmpl
new file mode 100644
index 0000000000..2e09f14e46
--- /dev/null
+++ b/templates/admin/perftrace.tmpl
@@ -0,0 +1,13 @@
+{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin monitor")}}
+
+<div class="admin-setting-content">
+	{{template "admin/trace_tabs" .}}
+
+	{{range $record := .PerfTraceRecords}}
+	<div class="ui segment tw-w-full tw-overflow-auto">
+		<pre class="tw-whitespace-pre">{{$record.Content}}</pre>
+	</div>
+	{{end}}
+</div>
+
+{{template "admin/layout_footer" .}}
diff --git a/templates/admin/stacktrace-row.tmpl b/templates/admin/stacktrace-row.tmpl
index 97c361ff90..db7ed81c79 100644
--- a/templates/admin/stacktrace-row.tmpl
+++ b/templates/admin/stacktrace-row.tmpl
@@ -17,7 +17,10 @@
 		</div>
 		<div>
 			{{if or (eq .Process.Type "request") (eq .Process.Type "normal")}}
-				<a class="delete-button icon" href="" data-url="{{.root.Link}}/cancel/{{.Process.PID}}" data-id="{{.Process.PID}}" data-name="{{.Process.Description}}">{{svg "octicon-trash" 16 "text-red"}}</a>
+				<a class="link-action" data-url="{{.root.Link}}/cancel/{{.Process.PID}}"
+					data-modal-confirm-header="{{ctx.Locale.Tr "admin.monitor.process.cancel"}}"
+					data-modal-confirm-content="{{ctx.Locale.Tr "admin.monitor.process.cancel_desc"}}"
+				>{{svg "octicon-trash" 16 "text-red"}}</a>
 			{{end}}
 		</div>
 	</div>
diff --git a/templates/admin/stacktrace.tmpl b/templates/admin/stacktrace.tmpl
index ce03d80555..c5dde6b30c 100644
--- a/templates/admin/stacktrace.tmpl
+++ b/templates/admin/stacktrace.tmpl
@@ -1,22 +1,7 @@
 {{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin monitor")}}
 <div class="admin-setting-content">
 
-	<div class="tw-flex tw-items-center">
-		<div class="tw-flex-1">
-			<div class="ui compact small menu">
-				<a class="{{if eq .ShowGoroutineList "process"}}active {{end}}item" href="?show=process">{{ctx.Locale.Tr "admin.monitor.process"}}</a>
-				<a class="{{if eq .ShowGoroutineList "stacktrace"}}active {{end}}item" href="?show=stacktrace">{{ctx.Locale.Tr "admin.monitor.stacktrace"}}</a>
-			</div>
-		</div>
-		<form target="_blank" action="{{AppSubUrl}}/-/admin/monitor/diagnosis" class="ui form">
-			<div class="ui inline field">
-				<button class="ui primary small button">{{ctx.Locale.Tr "admin.monitor.download_diagnosis_report"}}</button>
-				<input name="seconds" size="3" maxlength="3" value="10"> {{ctx.Locale.Tr "tool.raw_seconds"}}
-			</div>
-		</form>
-	</div>
-
-	<div class="divider"></div>
+	{{template "admin/trace_tabs" .}}
 
 	<h4 class="ui top attached header">
 		{{printf "%d Goroutines" .GoroutineCount}}{{/* Goroutine is non-translatable*/}}
@@ -34,15 +19,4 @@
 	{{end}}
 </div>
 
-<div class="ui g-modal-confirm delete modal">
-	<div class="header">
-		{{ctx.Locale.Tr "admin.monitor.process.cancel"}}
-	</div>
-	<div class="content">
-		<p>{{ctx.Locale.Tr "admin.monitor.process.cancel_notices" (`<span class="name"></span>`|SafeHTML)}}</p>
-		<p>{{ctx.Locale.Tr "admin.monitor.process.cancel_desc"}}</p>
-	</div>
-	{{template "base/modal_actions_confirm" .}}
-</div>
-
 {{template "admin/layout_footer" .}}
diff --git a/templates/admin/trace_tabs.tmpl b/templates/admin/trace_tabs.tmpl
new file mode 100644
index 0000000000..5066c9c41b
--- /dev/null
+++ b/templates/admin/trace_tabs.tmpl
@@ -0,0 +1,19 @@
+<div class="flex-text-block">
+	<div class="tw-flex-1">
+		<div class="ui compact small menu">
+			{{if .ShowAdminPerformanceTraceTab}}
+			<a class="item {{Iif .PageIsAdminMonitorPerfTrace "active"}}" href="{{AppSubUrl}}/-/admin/monitor/perftrace">{{ctx.Locale.Tr "admin.monitor.performance_logs"}}</a>
+			{{end}}
+			<a class="item {{Iif (eq .ShowGoroutineList "process") "active"}}" href="{{AppSubUrl}}/-/admin/monitor/stacktrace?show=process">{{ctx.Locale.Tr "admin.monitor.process"}}</a>
+			<a class="item {{Iif (eq .ShowGoroutineList "stacktrace") "active"}}" href="{{AppSubUrl}}/-/admin/monitor/stacktrace?show=stacktrace">{{ctx.Locale.Tr "admin.monitor.stacktrace"}}</a>
+		</div>
+	</div>
+	<form target="_blank" action="{{AppSubUrl}}/-/admin/monitor/diagnosis" class="ui form">
+		<div class="ui inline field">
+			<button class="ui primary small button">{{ctx.Locale.Tr "admin.monitor.download_diagnosis_report"}}</button>
+			<input name="seconds" size="3" maxlength="3" value="10"> {{ctx.Locale.Tr "tool.raw_seconds"}}
+		</div>
+	</form>
+</div>
+
+<div class="divider"></div>
diff --git a/templates/package/content/container.tmpl b/templates/package/content/container.tmpl
index 04732d276a..a88ebec3bc 100644
--- a/templates/package/content/container.tmpl
+++ b/templates/package/content/container.tmpl
@@ -24,7 +24,7 @@
 		</div>
 	</div>
 	{{if .PackageDescriptor.Metadata.Manifests}}
-		<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.container.multi_arch"}}</h4>
+		<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.container.images"}}</h4>
 		<div class="ui attached segment">
 			<table class="ui very basic compact table">
 				<thead>
diff --git a/templates/package/content/maven.tmpl b/templates/package/content/maven.tmpl
index f56595a830..e98fc53692 100644
--- a/templates/package/content/maven.tmpl
+++ b/templates/package/content/maven.tmpl
@@ -11,7 +11,7 @@
 				<div class="markup"><pre class="code-block"><code>&lt;repositories&gt;
 	&lt;repository&gt;
 		&lt;id&gt;gitea&lt;/id&gt;
-			&lt;url&gt;<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></origin-url>&lt;/url&gt;
+		&lt;url&gt;<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></origin-url>&lt;/url&gt;
 	&lt;/repository&gt;
 &lt;/repositories&gt;
 
diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl
index 25614d0df4..5d6a8ddf01 100644
--- a/templates/repo/branch/list.tmpl
+++ b/templates/repo/branch/list.tmpl
@@ -143,7 +143,7 @@
 									{{if .LatestPullRequest.HasMerged}}
 										<a href="{{.LatestPullRequest.Issue.Link}}" class="ui purple large label">{{svg "octicon-git-merge" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.pulls.merged"}}</a>
 									{{else if .LatestPullRequest.Issue.IsClosed}}
-										<a href="{{.LatestPullRequest.Issue.Link}}" class="ui red large label">{{svg "octicon-git-pull-request" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.issues.closed_title"}}</a>
+										<a href="{{.LatestPullRequest.Issue.Link}}" class="ui red large label">{{svg "octicon-git-pull-request-closed" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.issues.closed_title"}}</a>
 									{{else}}
 										<a href="{{.LatestPullRequest.Issue.Link}}" class="ui green large label">{{svg "octicon-git-pull-request" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.issues.open_title"}}</a>
 									{{end}}
diff --git a/templates/repo/clone_panel.tmpl b/templates/repo/clone_panel.tmpl
index c1c2c87b75..b813860150 100644
--- a/templates/repo/clone_panel.tmpl
+++ b/templates/repo/clone_panel.tmpl
@@ -1,5 +1,6 @@
 <button class="ui primary button js-btn-clone-panel">
-	<span>{{svg "octicon-code" 16}} Code</span>
+	{{svg "octicon-code" 16}}
+	<span>Code</span>
 	{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 </button>
 <div class="clone-panel-popup tippy-target">
diff --git a/templates/repo/code/upstream_diverging_info.tmpl b/templates/repo/code/upstream_diverging_info.tmpl
index a1f37b8c05..b3d35c05e5 100644
--- a/templates/repo/code/upstream_diverging_info.tmpl
+++ b/templates/repo/code/upstream_diverging_info.tmpl
@@ -1,10 +1,12 @@
-{{if and .UpstreamDivergingInfo (or .UpstreamDivergingInfo.BaseHasNewCommits .UpstreamDivergingInfo.CommitsBehind)}}
+{{if and .UpstreamDivergingInfo .UpstreamDivergingInfo.BaseBranchHasNewCommits}}
 <div class="ui message flex-text-block">
 	<div class="tw-flex-1">
-		{{$upstreamLink := printf "%s/src/branch/%s" .Repository.BaseRepo.Link (.Repository.BaseRepo.DefaultBranch|PathEscapeSegments)}}
-		{{$upstreamHtml := HTMLFormat `<a href="%s">%s:%s</a>` $upstreamLink .Repository.BaseRepo.FullName .Repository.BaseRepo.DefaultBranch}}
-		{{if .UpstreamDivergingInfo.CommitsBehind}}
-			{{ctx.Locale.TrN .UpstreamDivergingInfo.CommitsBehind "repo.pulls.upstream_diverging_prompt_behind_1" "repo.pulls.upstream_diverging_prompt_behind_n" .UpstreamDivergingInfo.CommitsBehind $upstreamHtml}}
+		{{$upstreamLink := printf "%s/src/branch/%s" .Repository.BaseRepo.Link (.UpstreamDivergingInfo.BaseBranchName|PathEscapeSegments)}}
+		{{$upstreamRepoBranchDisplay := HTMLFormat "%s:%s" .Repository.BaseRepo.FullName .UpstreamDivergingInfo.BaseBranchName}}
+		{{$thisRepoBranchDisplay := HTMLFormat "%s:%s" .Repository.FullName .BranchName}}
+		{{$upstreamHtml := HTMLFormat `<a href="%s">%s</a>` $upstreamLink $upstreamRepoBranchDisplay}}
+		{{if .UpstreamDivergingInfo.HeadBranchCommitsBehind}}
+			{{ctx.Locale.TrN .UpstreamDivergingInfo.HeadBranchCommitsBehind "repo.pulls.upstream_diverging_prompt_behind_1" "repo.pulls.upstream_diverging_prompt_behind_n" .UpstreamDivergingInfo.HeadBranchCommitsBehind $upstreamHtml}}
 		{{else}}
 			{{ctx.Locale.Tr "repo.pulls.upstream_diverging_prompt_base_newer" $upstreamHtml}}
 		{{end}}
@@ -12,7 +14,7 @@
 	{{if .CanWriteCode}}
 	<button class="ui compact primary button tw-m-0 link-action"
 					data-modal-confirm-header="{{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge"}}"
-					data-modal-confirm-content="{{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge_confirm" .BranchName}}"
+					data-modal-confirm-content="{{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge_confirm" $upstreamRepoBranchDisplay $thisRepoBranchDisplay}}"
 					data-url="{{.Repository.Link}}/branches/merge-upstream?branch={{.BranchName}}">
 		{{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge"}}
 	</button>
diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl
index 0354f6ef22..dcc1f48c2c 100644
--- a/templates/repo/issue/view_title.tmpl
+++ b/templates/repo/issue/view_title.tmpl
@@ -42,7 +42,7 @@
 		{{if .HasMerged}}
 			<div class="ui purple label issue-state-label">{{svg "octicon-git-merge" 16 "tw-mr-1"}} {{if eq .Issue.PullRequest.Status 3}}{{ctx.Locale.Tr "repo.pulls.manually_merged"}}{{else}}{{ctx.Locale.Tr "repo.pulls.merged"}}{{end}}</div>
 		{{else if .Issue.IsClosed}}
-			<div class="ui red label issue-state-label">{{svg (Iif .Issue.IsPull "octicon-git-pull-request" "octicon-issue-closed")}} {{ctx.Locale.Tr "repo.issues.closed_title"}}</div>
+			<div class="ui red label issue-state-label">{{svg (Iif .Issue.IsPull "octicon-git-pull-request-closed" "octicon-issue-closed")}} {{ctx.Locale.Tr "repo.issues.closed_title"}}</div>
 		{{else if .Issue.IsPull}}
 			{{if .IsPullWorkInProgress}}
 				<div class="ui grey label issue-state-label">{{svg "octicon-git-pull-request-draft"}} {{ctx.Locale.Tr "repo.issues.draft_title"}}</div>
diff --git a/templates/repo/sub_menu.tmpl b/templates/repo/sub_menu.tmpl
index 30244a8861..3533bfed0b 100644
--- a/templates/repo/sub_menu.tmpl
+++ b/templates/repo/sub_menu.tmpl
@@ -2,7 +2,7 @@
 <div class="ui segments repository-summary tw-mt-1 tw-mb-0">
 	<div class="ui segment sub-menu repository-menu">
 		{{if and (.Permission.CanRead ctx.Consts.RepoUnitTypeCode) (not .IsEmptyRepo)}}
-			<a class="item muted {{if .PageIsCommits}}active{{end}}" href="{{.RepoLink}}/commits/{{.RefTypeNameSubURL}}">
+			<a class="item muted {{if .PageIsCommits}}active{{end}}" href="{{.RepoLink}}/commits/{{.RefFullName.RefWebLinkPath}}">
 				{{svg "octicon-history"}} <b>{{ctx.Locale.PrettyNumber .CommitsCount}}</b> {{ctx.Locale.TrN .CommitsCount "repo.commit" "repo.commits"}}
 			</a>
 			<a class="item muted {{if .PageIsBranches}}active{{end}}" href="{{.RepoLink}}/branches">
diff --git a/templates/shared/issueicon.tmpl b/templates/shared/issueicon.tmpl
index f828de5c66..bb6247c708 100644
--- a/templates/shared/issueicon.tmpl
+++ b/templates/shared/issueicon.tmpl
@@ -1,3 +1,4 @@
+{{/* the logic should be kept the same as getIssueIcon/getIssueColor in JS code */}}
 {{- if .IsPull -}}
 	{{- if not .PullRequest -}}
 		No PullRequest
@@ -6,7 +7,7 @@
 			{{- if .PullRequest.HasMerged -}}
 				{{- svg "octicon-git-merge" 16 "text purple" -}}
 			{{- else -}}
-				{{- svg "octicon-git-pull-request" 16 "text red" -}}
+				{{- svg "octicon-git-pull-request-closed" 16 "text red" -}}
 			{{- end -}}
 		{{- else -}}
 			{{- if .PullRequest.IsWorkInProgress ctx -}}
diff --git a/templates/user/auth/link_account.tmpl b/templates/user/auth/link_account.tmpl
index a99e172d05..d244ce38c2 100644
--- a/templates/user/auth/link_account.tmpl
+++ b/templates/user/auth/link_account.tmpl
@@ -16,13 +16,18 @@
 		</div>
 	</overflow-menu>
 	<div class="ui middle very relaxed page grid">
-		<div class="column">
+		<div class="column tw-my-5">
+			{{/* these styles are quite tricky but it needs to be the same as the signin page */}}
 			<div class="ui tab {{if not .user_exists}}active{{end}}" data-tab="auth-link-signup-tab">
+				<div class="tw-flex tw-flex-col tw-gap-4 tw-max-w-2xl tw-m-auto">
 				{{if .AutoRegistrationFailedPrompt}}<div class="ui message">{{.AutoRegistrationFailedPrompt}}</div>{{end}}
 				{{template "user/auth/signup_inner" .}}
+				</div>
 			</div>
 			<div class="ui tab {{if .user_exists}}active{{end}}" data-tab="auth-link-signin-tab">
+				<div class="tw-flex tw-flex-col tw-gap-4 tw-max-w-2xl tw-m-auto">
 				{{template "user/auth/signin_inner" .}}
+				</div>
 			</div>
 		</div>
 	</div>
diff --git a/templates/user/auth/signin.tmpl b/templates/user/auth/signin.tmpl
index 54cc82d49d..75e1bb27f9 100644
--- a/templates/user/auth/signin.tmpl
+++ b/templates/user/auth/signin.tmpl
@@ -1,6 +1,7 @@
 {{template "base/head" .}}
 <div role="main" aria-label="{{.Title}}" class="page-content user signin{{if .LinkAccountMode}} icon{{end}}">
 	<div class="ui middle very relaxed page grid">
+		{{/* these styles are quite tricky and should also apply to the signup and link_account pages */}}
 		<div class="column tw-flex tw-flex-col tw-gap-4 tw-max-w-2xl tw-m-auto">
 			{{template "user/auth/signin_inner" .}}
 		</div>
diff --git a/templates/user/auth/signup_inner.tmpl b/templates/user/auth/signup_inner.tmpl
index b3b2a4205e..d66568199d 100644
--- a/templates/user/auth/signup_inner.tmpl
+++ b/templates/user/auth/signup_inner.tmpl
@@ -59,12 +59,12 @@
 </div>
 
 <div class="ui container fluid">
+	{{if not .LinkAccountMode}}
 	<div class="ui attached segment header top tw-flex tw-flex-col tw-items-center">
-		{{if not .LinkAccountMode}}
 		<div class="field">
 			<span>{{ctx.Locale.Tr "auth.already_have_account"}}</span>
 			<a href="{{AppSubUrl}}/user/login">{{ctx.Locale.Tr "auth.sign_in_now"}}</a>
 		</div>
-		{{end}}
 	</div>
+	{{end}}
 </div>
diff --git a/tests/e2e/utils_e2e.ts b/tests/e2e/utils_e2e.ts
index 14ec836600..3e92e0d3c2 100644
--- a/tests/e2e/utils_e2e.ts
+++ b/tests/e2e/utils_e2e.ts
@@ -1,12 +1,13 @@
 import {expect} from '@playwright/test';
 import {env} from 'node:process';
+import type {Browser, Page, WorkerInfo} from '@playwright/test';
 
 const ARTIFACTS_PATH = `tests/e2e/test-artifacts`;
 const LOGIN_PASSWORD = 'password';
 
 // log in user and store session info. This should generally be
 //  run in test.beforeAll(), then the session can be loaded in tests.
-export async function login_user(browser, workerInfo, user) {
+export async function login_user(browser: Browser, workerInfo: WorkerInfo, user: string) {
   // Set up a new context
   const context = await browser.newContext();
   const page = await context.newPage();
@@ -17,8 +18,8 @@ export async function login_user(browser, workerInfo, user) {
   expect(response?.status()).toBe(200); // Status OK
 
   // Fill out form
-  await page.type('input[name=user_name]', user);
-  await page.type('input[name=password]', LOGIN_PASSWORD);
+  await page.locator('input[name=user_name]').fill(user);
+  await page.locator('input[name=password]').fill(LOGIN_PASSWORD);
   await page.click('form button.ui.primary.button:visible');
 
   await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle
@@ -31,7 +32,7 @@ export async function login_user(browser, workerInfo, user) {
   return context;
 }
 
-export async function load_logged_in_context(browser, workerInfo, user) {
+export async function load_logged_in_context(browser: Browser, workerInfo: WorkerInfo, user: string) {
   let context;
   try {
     context = await browser.newContext({storageState: `${ARTIFACTS_PATH}/state-${user}-${workerInfo.workerIndex}.json`});
@@ -43,7 +44,7 @@ export async function load_logged_in_context(browser, workerInfo, user) {
   return context;
 }
 
-export async function save_visual(page) {
+export async function save_visual(page: Page) {
   // Optionally include visual testing
   if (env.VISUAL_TEST) {
     await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle
diff --git a/tests/integration/api_branch_test.go b/tests/integration/api_branch_test.go
index 8a0bd2e4ff..d64dd97f93 100644
--- a/tests/integration/api_branch_test.go
+++ b/tests/integration/api_branch_test.go
@@ -190,28 +190,61 @@ func testAPICreateBranch(t testing.TB, session *TestSession, user, repo, oldBran
 func TestAPIUpdateBranch(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, _ *url.URL) {
 		t.Run("UpdateBranchWithEmptyRepo", func(t *testing.T) {
-			testAPIUpdateBranch(t, "user10", "repo6", "master", "test", http.StatusNotFound)
+			testAPIUpdateBranch(t, "user10", "user10", "repo6", "master", "test", http.StatusNotFound)
 		})
 		t.Run("UpdateBranchWithSameBranchNames", func(t *testing.T) {
-			resp := testAPIUpdateBranch(t, "user2", "repo1", "master", "master", http.StatusUnprocessableEntity)
+			resp := testAPIUpdateBranch(t, "user2", "user2", "repo1", "master", "master", http.StatusUnprocessableEntity)
 			assert.Contains(t, resp.Body.String(), "Cannot rename a branch using the same name or rename to a branch that already exists.")
 		})
 		t.Run("UpdateBranchThatAlreadyExists", func(t *testing.T) {
-			resp := testAPIUpdateBranch(t, "user2", "repo1", "master", "branch2", http.StatusUnprocessableEntity)
+			resp := testAPIUpdateBranch(t, "user2", "user2", "repo1", "master", "branch2", http.StatusUnprocessableEntity)
 			assert.Contains(t, resp.Body.String(), "Cannot rename a branch using the same name or rename to a branch that already exists.")
 		})
 		t.Run("UpdateBranchWithNonExistentBranch", func(t *testing.T) {
-			resp := testAPIUpdateBranch(t, "user2", "repo1", "i-dont-exist", "new-branch-name", http.StatusNotFound)
+			resp := testAPIUpdateBranch(t, "user2", "user2", "repo1", "i-dont-exist", "new-branch-name", http.StatusNotFound)
 			assert.Contains(t, resp.Body.String(), "Branch doesn't exist.")
 		})
-		t.Run("RenameBranchNormalScenario", func(t *testing.T) {
-			testAPIUpdateBranch(t, "user2", "repo1", "branch2", "new-branch-name", http.StatusNoContent)
+		t.Run("UpdateBranchWithNonAdminDoer", func(t *testing.T) {
+			// don't allow default branch renaming
+			resp := testAPIUpdateBranch(t, "user40", "user2", "repo1", "master", "new-branch-name", http.StatusForbidden)
+			assert.Contains(t, resp.Body.String(), "User must be a repo or site admin to rename default or protected branches.")
+
+			// don't allow protected branch renaming
+			token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteRepository)
+			req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/branches", &api.CreateBranchRepoOption{
+				BranchName: "protected-branch",
+			}).AddTokenAuth(token)
+			MakeRequest(t, req, http.StatusCreated)
+			testAPICreateBranchProtection(t, "protected-branch", 1, http.StatusCreated)
+			resp = testAPIUpdateBranch(t, "user40", "user2", "repo1", "protected-branch", "new-branch-name", http.StatusForbidden)
+			assert.Contains(t, resp.Body.String(), "User must be a repo or site admin to rename default or protected branches.")
+		})
+		t.Run("UpdateBranchWithGlobedBasedProtectionRulesAndAdminAccess", func(t *testing.T) {
+			// don't allow branch that falls under glob-based protection rules to be renamed
+			token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteRepository)
+			req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/branch_protections", &api.BranchProtection{
+				RuleName:   "protected/**",
+				EnablePush: true,
+			}).AddTokenAuth(token)
+			MakeRequest(t, req, http.StatusCreated)
+
+			from := "protected/1"
+			req = NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/branches", &api.CreateBranchRepoOption{
+				BranchName: from,
+			}).AddTokenAuth(token)
+			MakeRequest(t, req, http.StatusCreated)
+
+			resp := testAPIUpdateBranch(t, "user2", "user2", "repo1", from, "new-branch-name", http.StatusForbidden)
+			assert.Contains(t, resp.Body.String(), "Branch is protected by glob-based protection rules.")
+		})
+		t.Run("UpdateBranchNormalScenario", func(t *testing.T) {
+			testAPIUpdateBranch(t, "user2", "user2", "repo1", "branch2", "new-branch-name", http.StatusNoContent)
 		})
 	})
 }
 
-func testAPIUpdateBranch(t *testing.T, ownerName, repoName, from, to string, expectedHTTPStatus int) *httptest.ResponseRecorder {
-	token := getUserToken(t, ownerName, auth_model.AccessTokenScopeWriteRepository)
+func testAPIUpdateBranch(t *testing.T, doerName, ownerName, repoName, from, to string, expectedHTTPStatus int) *httptest.ResponseRecorder {
+	token := getUserToken(t, doerName, auth_model.AccessTokenScopeWriteRepository)
 	req := NewRequestWithJSON(t, "PATCH", "api/v1/repos/"+ownerName+"/"+repoName+"/branches/"+from, &api.UpdateBranchRepoOption{
 		Name: to,
 	}).AddTokenAuth(token)
diff --git a/tests/integration/api_repo_test.go b/tests/integration/api_repo_test.go
index 122afbfa08..13dc90f8a7 100644
--- a/tests/integration/api_repo_test.go
+++ b/tests/integration/api_repo_test.go
@@ -735,5 +735,5 @@ func TestAPIRepoGetAssignees(t *testing.T) {
 	resp := MakeRequest(t, req, http.StatusOK)
 	var assignees []*api.User
 	DecodeJSON(t, resp, &assignees)
-	assert.Len(t, assignees, 1)
+	assert.Len(t, assignees, 2)
 }
diff --git a/tests/integration/repo_branch_test.go b/tests/integration/repo_branch_test.go
index 2b4c417334..f9cf13112a 100644
--- a/tests/integration/repo_branch_test.go
+++ b/tests/integration/repo_branch_test.go
@@ -11,13 +11,8 @@ import (
 	"strings"
 	"testing"
 
-	auth_model "code.gitea.io/gitea/models/auth"
-	org_model "code.gitea.io/gitea/models/organization"
-	"code.gitea.io/gitea/models/perm"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/models/unittest"
-	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/test"
 	"code.gitea.io/gitea/modules/translation"
 	"code.gitea.io/gitea/tests"
@@ -142,19 +137,51 @@ func TestCreateBranchInvalidCSRF(t *testing.T) {
 	assert.Contains(t, resp.Body.String(), "Invalid CSRF token")
 }
 
-func prepareBranch(t *testing.T, session *TestSession, repo *repo_model.Repository) {
-	baseRefSubURL := fmt.Sprintf("branch/%s", repo.DefaultBranch)
-
+func prepareRecentlyPushedBranchTest(t *testing.T, headSession *TestSession, baseRepo, headRepo *repo_model.Repository) {
+	refSubURL := fmt.Sprintf("branch/%s", headRepo.DefaultBranch)
+	baseRepoPath := baseRepo.OwnerName + "/" + baseRepo.Name
+	headRepoPath := headRepo.OwnerName + "/" + headRepo.Name
+	// Case 1: Normal branch changeset to display pushed message
 	// create branch with no new commit
-	testCreateBranch(t, session, repo.OwnerName, repo.Name, baseRefSubURL, "no-commit", http.StatusSeeOther)
+	testCreateBranch(t, headSession, headRepo.OwnerName, headRepo.Name, refSubURL, "no-commit", http.StatusSeeOther)
 
 	// create branch with commit
-	testCreateBranch(t, session, repo.OwnerName, repo.Name, baseRefSubURL, "new-commit", http.StatusSeeOther)
-	testAPINewFile(t, session, repo.OwnerName, repo.Name, "new-commit", "new-commit.txt", "new-commit")
+	testAPINewFile(t, headSession, headRepo.OwnerName, headRepo.Name, "new-commit", fmt.Sprintf("new-file-%s.txt", headRepo.Name), "new-commit")
 
-	// create deleted branch
-	testCreateBranch(t, session, repo.OwnerName, repo.Name, "branch/new-commit", "deleted-branch", http.StatusSeeOther)
-	testUIDeleteBranch(t, session, repo.OwnerName, repo.Name, "deleted-branch")
+	// create a branch then delete it
+	testCreateBranch(t, headSession, headRepo.OwnerName, headRepo.Name, "branch/new-commit", "deleted-branch", http.StatusSeeOther)
+	testUIDeleteBranch(t, headSession, headRepo.OwnerName, headRepo.Name, "deleted-branch")
+
+	// only `new-commit` branch has commits ahead the base branch
+	checkRecentlyPushedNewBranches(t, headSession, headRepoPath, []string{"new-commit"})
+	if baseRepo.RepoPath() != headRepo.RepoPath() {
+		checkRecentlyPushedNewBranches(t, headSession, baseRepoPath, []string{fmt.Sprintf("%v:new-commit", headRepo.FullName())})
+	}
+
+	// Case 2: Create PR so that `new-commit` branch will not show
+	testCreatePullToDefaultBranch(t, headSession, baseRepo, headRepo, "new-commit", "merge new-commit to default branch")
+	// No push message show because of active PR
+	checkRecentlyPushedNewBranches(t, headSession, headRepoPath, []string{})
+	if baseRepo.RepoPath() != headRepo.RepoPath() {
+		checkRecentlyPushedNewBranches(t, headSession, baseRepoPath, []string{})
+	}
+}
+
+func prepareRecentlyPushedBranchSpecialTest(t *testing.T, session *TestSession, baseRepo, headRepo *repo_model.Repository) {
+	refSubURL := fmt.Sprintf("branch/%s", headRepo.DefaultBranch)
+	baseRepoPath := baseRepo.OwnerName + "/" + baseRepo.Name
+	headRepoPath := headRepo.OwnerName + "/" + headRepo.Name
+	// create branch with no new commit
+	testCreateBranch(t, session, headRepo.OwnerName, headRepo.Name, refSubURL, "no-commit-special", http.StatusSeeOther)
+
+	// update base (default) branch before head branch is updated
+	testAPINewFile(t, session, baseRepo.OwnerName, baseRepo.Name, baseRepo.DefaultBranch, fmt.Sprintf("new-file-special-%s.txt", headRepo.Name), "new-commit")
+
+	// Though we have new `no-commit` branch, but the headBranch is not newer or commits ahead baseBranch. No message show.
+	checkRecentlyPushedNewBranches(t, session, headRepoPath, []string{})
+	if baseRepo.RepoPath() != headRepo.RepoPath() {
+		checkRecentlyPushedNewBranches(t, session, baseRepoPath, []string{})
+	}
 }
 
 func testCreatePullToDefaultBranch(t *testing.T, session *TestSession, baseRepo, headRepo *repo_model.Repository, headBranch, title string) string {
@@ -169,6 +196,9 @@ func testCreatePullToDefaultBranch(t *testing.T, session *TestSession, baseRepo,
 }
 
 func prepareRepoPR(t *testing.T, baseSession, headSession *TestSession, baseRepo, headRepo *repo_model.Repository) {
+	refSubURL := fmt.Sprintf("branch/%s", headRepo.DefaultBranch)
+	testCreateBranch(t, headSession, headRepo.OwnerName, headRepo.Name, refSubURL, "new-commit", http.StatusSeeOther)
+
 	// create opening PR
 	testCreateBranch(t, headSession, headRepo.OwnerName, headRepo.Name, "branch/new-commit", "opening-pr", http.StatusSeeOther)
 	testCreatePullToDefaultBranch(t, baseSession, baseRepo, headRepo, "opening-pr", "opening pr")
@@ -210,65 +240,19 @@ func checkRecentlyPushedNewBranches(t *testing.T, session *TestSession, repoPath
 
 func TestRecentlyPushedNewBranches(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, u *url.URL) {
-		user1Session := loginUser(t, "user1")
-		user2Session := loginUser(t, "user2")
 		user12Session := loginUser(t, "user12")
-		user13Session := loginUser(t, "user13")
 
-		// prepare branch and PRs in original repo
+		// Same reposioty check
 		repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
-		prepareBranch(t, user12Session, repo10)
 		prepareRepoPR(t, user12Session, user12Session, repo10, repo10)
-
-		// outdated new branch should not be displayed
-		checkRecentlyPushedNewBranches(t, user12Session, "user12/repo10", []string{"new-commit"})
+		prepareRecentlyPushedBranchTest(t, user12Session, repo10, repo10)
+		prepareRecentlyPushedBranchSpecialTest(t, user12Session, repo10, repo10)
 
 		// create a fork repo in public org
-		testRepoFork(t, user12Session, repo10.OwnerName, repo10.Name, "org25", "org25_fork_repo10", "new-commit")
+		testRepoFork(t, user12Session, repo10.OwnerName, repo10.Name, "org25", "org25_fork_repo10", repo10.DefaultBranch)
 		orgPublicForkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 25, Name: "org25_fork_repo10"})
 		prepareRepoPR(t, user12Session, user12Session, repo10, orgPublicForkRepo)
-
-		// user12 is the owner of the repo10 and the organization org25
-		// in repo10, user12 has opening/closed/merged pr and closed/merged pr with deleted branch
-		checkRecentlyPushedNewBranches(t, user12Session, "user12/repo10", []string{"org25/org25_fork_repo10:new-commit", "new-commit"})
-
-		userForkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11})
-		testCtx := NewAPITestContext(t, repo10.OwnerName, repo10.Name, auth_model.AccessTokenScopeWriteRepository)
-		t.Run("AddUser13AsCollaborator", doAPIAddCollaborator(testCtx, "user13", perm.AccessModeWrite))
-		prepareBranch(t, user13Session, userForkRepo)
-		prepareRepoPR(t, user13Session, user13Session, repo10, userForkRepo)
-
-		// create branch with same name in different repo by user13
-		testCreateBranch(t, user13Session, repo10.OwnerName, repo10.Name, "branch/new-commit", "same-name-branch", http.StatusSeeOther)
-		testCreateBranch(t, user13Session, userForkRepo.OwnerName, userForkRepo.Name, "branch/new-commit", "same-name-branch", http.StatusSeeOther)
-		testCreatePullToDefaultBranch(t, user13Session, repo10, userForkRepo, "same-name-branch", "same name branch pr")
-
-		// user13 pushed 2 branches with the same name in repo10 and repo11
-		// and repo11's branch has a pr, but repo10's branch doesn't
-		// in this case, we should get repo10's branch but not repo11's branch
-		checkRecentlyPushedNewBranches(t, user13Session, "user12/repo10", []string{"same-name-branch", "user13/repo11:new-commit"})
-
-		// create a fork repo in private org
-		testRepoFork(t, user1Session, repo10.OwnerName, repo10.Name, "private_org35", "org35_fork_repo10", "new-commit")
-		orgPrivateForkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 35, Name: "org35_fork_repo10"})
-		prepareRepoPR(t, user1Session, user1Session, repo10, orgPrivateForkRepo)
-
-		// user1 is the owner of private_org35 and no write permission to repo10
-		// so user1 can only see the branch in org35_fork_repo10
-		checkRecentlyPushedNewBranches(t, user1Session, "user12/repo10", []string{"private_org35/org35_fork_repo10:new-commit"})
-
-		// user2 push a branch in private_org35
-		testCreateBranch(t, user2Session, orgPrivateForkRepo.OwnerName, orgPrivateForkRepo.Name, "branch/new-commit", "user-read-permission", http.StatusSeeOther)
-		// convert write permission to read permission for code unit
-		token := getTokenForLoggedInUser(t, user1Session, auth_model.AccessTokenScopeWriteOrganization)
-		req := NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/teams/%d", 24), &api.EditTeamOption{
-			Name:     "team24",
-			UnitsMap: map[string]string{"repo.code": "read"},
-		}).AddTokenAuth(token)
-		MakeRequest(t, req, http.StatusOK)
-		teamUnit := unittest.AssertExistsAndLoadBean(t, &org_model.TeamUnit{TeamID: 24, Type: unit.TypeCode})
-		assert.Equal(t, perm.AccessModeRead, teamUnit.AccessMode)
-		// user2 can see the branch as it is created by user2
-		checkRecentlyPushedNewBranches(t, user2Session, "user12/repo10", []string{"private_org35/org35_fork_repo10:user-read-permission"})
+		prepareRecentlyPushedBranchTest(t, user12Session, repo10, orgPublicForkRepo)
+		prepareRecentlyPushedBranchSpecialTest(t, user12Session, repo10, orgPublicForkRepo)
 	})
 }
diff --git a/tests/integration/repo_merge_upstream_test.go b/tests/integration/repo_merge_upstream_test.go
index e3e423c51d..e928b04e9b 100644
--- a/tests/integration/repo_merge_upstream_test.go
+++ b/tests/integration/repo_merge_upstream_test.go
@@ -60,25 +60,54 @@ func TestRepoMergeUpstream(t *testing.T) {
 
 		t.Run("HeadBeforeBase", func(t *testing.T) {
 			// add a file in base repo
+			sessionBaseUser := loginUser(t, baseUser.Name)
 			require.NoError(t, createOrReplaceFileInBranch(baseUser, baseRepo, "new-file.txt", "master", "test-content-1"))
 
-			// the repo shows a prompt to "sync fork"
 			var mergeUpstreamLink string
-			require.Eventually(t, func() bool {
-				resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK)
-				htmlDoc := NewHTMLParser(t, resp.Body)
-				mergeUpstreamLink = queryMergeUpstreamButtonLink(htmlDoc)
-				if mergeUpstreamLink == "" {
-					return false
-				}
-				respMsg, _ := htmlDoc.Find(".ui.message:not(.positive)").Html()
-				return strings.Contains(respMsg, `This branch is 1 commit behind <a href="/user2/repo1/src/branch/master">user2/repo1:master</a>`)
-			}, 5*time.Second, 100*time.Millisecond)
+			t.Run("DetectDefaultBranch", func(t *testing.T) {
+				// the repo shows a prompt to "sync fork" (defaults to the default branch)
+				require.Eventually(t, func() bool {
+					resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK)
+					htmlDoc := NewHTMLParser(t, resp.Body)
+					mergeUpstreamLink = queryMergeUpstreamButtonLink(htmlDoc)
+					if mergeUpstreamLink == "" {
+						return false
+					}
+					respMsg, _ := htmlDoc.Find(".ui.message:not(.positive)").Html()
+					return strings.Contains(respMsg, `This branch is 1 commit behind <a href="/user2/repo1/src/branch/master">user2/repo1:master</a>`)
+				}, 5*time.Second, 100*time.Millisecond)
+			})
+
+			t.Run("DetectSameBranch", func(t *testing.T) {
+				// if the fork-branch name also exists in the base repo, then use that branch instead
+				req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/_new/branch/master", map[string]string{
+					"_csrf":           GetUserCSRFToken(t, sessionBaseUser),
+					"new_branch_name": "fork-branch",
+				})
+				sessionBaseUser.MakeRequest(t, req, http.StatusSeeOther)
+
+				require.Eventually(t, func() bool {
+					resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK)
+					htmlDoc := NewHTMLParser(t, resp.Body)
+					mergeUpstreamLink = queryMergeUpstreamButtonLink(htmlDoc)
+					if mergeUpstreamLink == "" {
+						return false
+					}
+					respMsg, _ := htmlDoc.Find(".ui.message:not(.positive)").Html()
+					return strings.Contains(respMsg, `This branch is 1 commit behind <a href="/user2/repo1/src/branch/fork-branch">user2/repo1:fork-branch</a>`)
+				}, 5*time.Second, 100*time.Millisecond)
+			})
 
 			// click the "sync fork" button
 			req = NewRequestWithValues(t, "POST", mergeUpstreamLink, map[string]string{"_csrf": GetUserCSRFToken(t, session)})
 			session.MakeRequest(t, req, http.StatusOK)
 			checkFileContent("fork-branch", "test-content-1")
+
+			// delete the "fork-branch" from the base repo
+			req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/delete?name=fork-branch", map[string]string{
+				"_csrf": GetUserCSRFToken(t, sessionBaseUser),
+			})
+			sessionBaseUser.MakeRequest(t, req, http.StatusOK)
 		})
 
 		t.Run("BaseChangeAfterHeadChange", func(t *testing.T) {
diff --git a/tests/integration/signin_test.go b/tests/integration/signin_test.go
index d7c0b1bcd3..a76287ca94 100644
--- a/tests/integration/signin_test.go
+++ b/tests/integration/signin_test.go
@@ -15,8 +15,11 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/test"
 	"code.gitea.io/gitea/modules/translation"
+	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/tests"
 
+	"github.com/markbates/goth"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@ -98,6 +101,11 @@ func TestSigninWithRememberMe(t *testing.T) {
 func TestEnablePasswordSignInForm(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 
+	mockLinkAccount := func(ctx *context.Context) {
+		gothUser := goth.User{Email: "invalid-email", Name: "."}
+		_ = ctx.Session.Set("linkAccountGothUser", gothUser)
+	}
+
 	t.Run("EnablePasswordSignInForm=false", func(t *testing.T) {
 		defer tests.PrintCurrentTest(t)()
 		defer test.MockVariableValue(&setting.Service.EnablePasswordSignInForm, false)()
@@ -108,6 +116,12 @@ func TestEnablePasswordSignInForm(t *testing.T) {
 
 		req = NewRequest(t, "POST", "/user/login")
 		MakeRequest(t, req, http.StatusForbidden)
+
+		req = NewRequest(t, "GET", "/user/link_account")
+		defer web.RouteMockReset()
+		web.RouteMock(web.MockAfterMiddlewares, mockLinkAccount)
+		resp = MakeRequest(t, req, http.StatusOK)
+		NewHTMLParser(t, resp.Body).AssertElement(t, "form[action='/user/link_account_signin']", false)
 	})
 
 	t.Run("EnablePasswordSignInForm=true", func(t *testing.T) {
@@ -120,5 +134,11 @@ func TestEnablePasswordSignInForm(t *testing.T) {
 
 		req = NewRequest(t, "POST", "/user/login")
 		MakeRequest(t, req, http.StatusOK)
+
+		req = NewRequest(t, "GET", "/user/link_account")
+		defer web.RouteMockReset()
+		web.RouteMock(web.MockAfterMiddlewares, mockLinkAccount)
+		resp = MakeRequest(t, req, http.StatusOK)
+		NewHTMLParser(t, resp.Body).AssertElement(t, "form[action='/user/link_account_signin']", true)
 	})
 }
diff --git a/tsconfig.json b/tsconfig.json
index d32cca0aaa..78b74a3d3c 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -23,6 +23,8 @@
     "stripInternal": true,
     "strict": false,
     "strictFunctionTypes": true,
+    "noImplicitAny": true,
+    "noImplicitThis": true,
     "noUnusedLocals": true,
     "noUnusedParameters": true,
     "noPropertyAccessFromIndexSignature": false,
diff --git a/updates.config.js b/updates.config.js
index a4a2fa5228..4ef1ca701b 100644
--- a/updates.config.js
+++ b/updates.config.js
@@ -3,6 +3,7 @@ export default {
     '@mcaptcha/vanilla-glue', // breaking changes in rc versions need to be handled
     'eslint', // need to migrate to eslint flat config first
     'eslint-plugin-array-func', // need to migrate to eslint flat config first
+    'eslint-plugin-github', // need to migrate to eslint 9 - https://github.com/github/eslint-plugin-github/issues/585
     'eslint-plugin-no-use-extend-native', // need to migrate to eslint flat config first
     'eslint-plugin-vitest', // need to migrate to eslint flat config first
   ],
diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue
index 41793d60ed..876292fc94 100644
--- a/web_src/js/components/DashboardRepoList.vue
+++ b/web_src/js/components/DashboardRepoList.vue
@@ -1,5 +1,5 @@
 <script lang="ts">
-import {createApp, nextTick} from 'vue';
+import {nextTick, defineComponent} from 'vue';
 import {SvgIcon} from '../svg.ts';
 import {GET} from '../modules/fetch.ts';
 import {fomanticQuery} from '../modules/fomantic/base.ts';
@@ -24,7 +24,7 @@ const commitStatus: CommitStatusMap = {
   warning: {name: 'gitea-exclamation', color: 'yellow'},
 };
 
-const sfc = {
+export default defineComponent({
   components: {SvgIcon},
   data() {
     const params = new URLSearchParams(window.location.search);
@@ -130,12 +130,12 @@ const sfc = {
   },
 
   methods: {
-    changeTab(t) {
-      this.tab = t;
+    changeTab(tab: string) {
+      this.tab = tab;
       this.updateHistory();
     },
 
-    changeReposFilter(filter) {
+    changeReposFilter(filter: string) {
       this.reposFilter = filter;
       this.repos = [];
       this.page = 1;
@@ -218,7 +218,7 @@ const sfc = {
       this.searchRepos();
     },
 
-    changePage(page) {
+    changePage(page: number) {
       this.page = page;
       if (this.page > this.finalPage) {
         this.page = this.finalPage;
@@ -256,7 +256,7 @@ const sfc = {
       }
 
       if (searchedURL === this.searchURL) {
-        this.repos = json.data.map((webSearchRepo) => {
+        this.repos = json.data.map((webSearchRepo: any) => {
           return {
             ...webSearchRepo.repository,
             latest_commit_status_state: webSearchRepo.latest_commit_status?.State, // if latest_commit_status is null, it means there is no commit status
@@ -264,7 +264,7 @@ const sfc = {
             locale_latest_commit_status_state: webSearchRepo.locale_latest_commit_status,
           };
         });
-        const count = response.headers.get('X-Total-Count');
+        const count = Number(response.headers.get('X-Total-Count'));
         if (searchedQuery === '' && searchedMode === '' && this.archivedFilter === 'both') {
           this.reposTotalCount = count;
         }
@@ -275,7 +275,7 @@ const sfc = {
       }
     },
 
-    repoIcon(repo) {
+    repoIcon(repo: any) {
       if (repo.fork) {
         return 'octicon-repo-forked';
       } else if (repo.mirror) {
@@ -298,7 +298,7 @@ const sfc = {
       return commitStatus[status].color;
     },
 
-    reposFilterKeyControl(e) {
+    reposFilterKeyControl(e: KeyboardEvent) {
       switch (e.key) {
         case 'Enter':
           document.querySelector<HTMLAnchorElement>('.repo-owner-name-list li.active a')?.click();
@@ -335,16 +335,8 @@ const sfc = {
       }
     },
   },
-};
+});
 
-export function initDashboardRepoList() {
-  const el = document.querySelector('#dashboard-repo-list');
-  if (el) {
-    createApp(sfc).mount(el);
-  }
-}
-
-export default sfc; // activate the IDE's Vue plugin
 </script>
 <template>
   <div>
diff --git a/web_src/js/components/DiffCommitSelector.vue b/web_src/js/components/DiffCommitSelector.vue
index 3a394955ca..16760d1cb1 100644
--- a/web_src/js/components/DiffCommitSelector.vue
+++ b/web_src/js/components/DiffCommitSelector.vue
@@ -1,9 +1,26 @@
 <script lang="ts">
+import {defineComponent} from 'vue';
 import {SvgIcon} from '../svg.ts';
 import {GET} from '../modules/fetch.ts';
 import {generateAriaId} from '../modules/fomantic/base.ts';
 
-export default {
+type Commit = {
+  id: string,
+  hovered: boolean,
+  selected: boolean,
+  summary: string,
+  committer_or_author_name: string,
+  time: string,
+  short_sha: string,
+}
+
+type CommitListResult = {
+  commits: Array<Commit>,
+  last_review_commit_sha: string,
+  locale: Record<string, string>,
+}
+
+export default defineComponent({
   components: {SvgIcon},
   data: () => {
     const el = document.querySelector('#diff-commit-select');
@@ -15,9 +32,9 @@ export default {
       locale: {
         filter_changes_by_commit: el.getAttribute('data-filter_changes_by_commit'),
       } as Record<string, string>,
-      commits: [],
+      commits: [] as Array<Commit>,
       hoverActivated: false,
-      lastReviewCommitSha: null,
+      lastReviewCommitSha: '',
       uniqueIdMenu: generateAriaId(),
       uniqueIdShowAll: generateAriaId(),
     };
@@ -55,11 +72,11 @@ export default {
       switch (event.key) {
         case 'ArrowDown': // select next element
           event.preventDefault();
-          this.focusElem(item.nextElementSibling, item);
+          this.focusElem(item.nextElementSibling as HTMLElement, item);
           break;
         case 'ArrowUp': // select previous element
           event.preventDefault();
-          this.focusElem(item.previousElementSibling, item);
+          this.focusElem(item.previousElementSibling as HTMLElement, item);
           break;
         case 'Escape': // close menu
           event.preventDefault();
@@ -70,7 +87,7 @@ export default {
       if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
         const item = document.activeElement; // try to highlight the selected commits
         const commitIdx = item?.matches('.item') ? item.getAttribute('data-commit-idx') : null;
-        if (commitIdx) this.highlight(this.commits[commitIdx]);
+        if (commitIdx) this.highlight(this.commits[Number(commitIdx)]);
       }
     },
     onKeyUp(event: KeyboardEvent) {
@@ -86,7 +103,7 @@ export default {
         }
       }
     },
-    highlight(commit) {
+    highlight(commit: Commit) {
       if (!this.hoverActivated) return;
       const indexSelected = this.commits.findIndex((x) => x.selected);
       const indexCurrentElem = this.commits.findIndex((x) => x.id === commit.id);
@@ -118,16 +135,17 @@ export default {
       // set correct tabindex to allow easier navigation
       this.$nextTick(() => {
         if (this.menuVisible) {
-          this.focusElem(this.$refs.showAllChanges, this.$refs.expandBtn);
+          this.focusElem(this.$refs.showAllChanges as HTMLElement, this.$refs.expandBtn as HTMLElement);
         } else {
-          this.focusElem(this.$refs.expandBtn, this.$refs.showAllChanges);
+          this.focusElem(this.$refs.expandBtn as HTMLElement, this.$refs.showAllChanges as HTMLElement);
         }
       });
     },
+
     /** Load the commits to show in this dropdown */
     async fetchCommits() {
       const resp = await GET(`${this.issueLink}/commits/list`);
-      const results = await resp.json();
+      const results = await resp.json() as CommitListResult;
       this.commits.push(...results.commits.map((x) => {
         x.hovered = false;
         return x;
@@ -165,7 +183,7 @@ export default {
      * the diff from beginning of PR up to the second clicked commit is
      * opened
      */
-    commitClickedShift(commit) {
+    commitClickedShift(commit: Commit) {
       this.hoverActivated = !this.hoverActivated;
       commit.selected = true;
       // Second click -> determine our range and open links accordingly
@@ -188,7 +206,7 @@ export default {
       }
     },
   },
-};
+});
 </script>
 <template>
   <div class="ui scrolling dropdown custom diff-commit-selector">
diff --git a/web_src/js/components/DiffFileList.vue b/web_src/js/components/DiffFileList.vue
index 2888c53d2e..6570c92781 100644
--- a/web_src/js/components/DiffFileList.vue
+++ b/web_src/js/components/DiffFileList.vue
@@ -17,18 +17,18 @@ function toggleFileList() {
   store.fileListIsVisible = !store.fileListIsVisible;
 }
 
-function diffTypeToString(pType) {
-  const diffTypes = {
-    1: 'add',
-    2: 'modify',
-    3: 'del',
-    4: 'rename',
-    5: 'copy',
+function diffTypeToString(pType: number) {
+  const diffTypes: Record<string, string> = {
+    '1': 'add',
+    '2': 'modify',
+    '3': 'del',
+    '4': 'rename',
+    '5': 'copy',
   };
-  return diffTypes[pType];
+  return diffTypes[String(pType)];
 }
 
-function diffStatsWidth(adds, dels) {
+function diffStatsWidth(adds: number, dels: number) {
   return `${adds / (adds + dels) * 100}%`;
 }
 
diff --git a/web_src/js/components/DiffFileTree.vue b/web_src/js/components/DiffFileTree.vue
index 9eabc65ae9..d00d03565f 100644
--- a/web_src/js/components/DiffFileTree.vue
+++ b/web_src/js/components/DiffFileTree.vue
@@ -1,5 +1,5 @@
 <script lang="ts" setup>
-import DiffFileTreeItem from './DiffFileTreeItem.vue';
+import DiffFileTreeItem, {type Item} from './DiffFileTreeItem.vue';
 import {loadMoreFiles} from '../features/repo-diff.ts';
 import {toggleElem} from '../utils/dom.ts';
 import {diffTreeStore} from '../modules/stores.ts';
@@ -11,7 +11,7 @@ const LOCAL_STORAGE_KEY = 'diff_file_tree_visible';
 const store = diffTreeStore();
 
 const fileTree = computed(() => {
-  const result = [];
+  const result: Array<Item> = [];
   for (const file of store.files) {
     // Split file into directories
     const splits = file.Name.split('/');
@@ -24,15 +24,10 @@ const fileTree = computed(() => {
       if (index === splits.length) {
         isFile = true;
       }
-      let newParent = {
+      let newParent: Item = {
         name: split,
         children: [],
         isFile,
-      } as {
-        name: string,
-        children: any[],
-        isFile: boolean,
-        file?: any,
       };
 
       if (isFile === true) {
@@ -60,7 +55,7 @@ const fileTree = computed(() => {
       parent = newParent;
     }
   }
-  const mergeChildIfOnlyOneDir = (entries) => {
+  const mergeChildIfOnlyOneDir = (entries: Array<Record<string, any>>) => {
     for (const entry of entries) {
       if (entry.children) {
         mergeChildIfOnlyOneDir(entry.children);
@@ -110,13 +105,13 @@ function toggleVisibility() {
   updateVisibility(!store.fileTreeIsVisible);
 }
 
-function updateVisibility(visible) {
+function updateVisibility(visible: boolean) {
   store.fileTreeIsVisible = visible;
   localStorage.setItem(LOCAL_STORAGE_KEY, store.fileTreeIsVisible);
   updateState(store.fileTreeIsVisible);
 }
 
-function updateState(visible) {
+function updateState(visible: boolean) {
   const btn = document.querySelector('.diff-toggle-file-tree-button');
   const [toShow, toHide] = btn.querySelectorAll('.icon');
   const tree = document.querySelector('#diff-file-tree');
diff --git a/web_src/js/components/DiffFileTreeItem.vue b/web_src/js/components/DiffFileTreeItem.vue
index 31ce94aacd..d3be10e3e9 100644
--- a/web_src/js/components/DiffFileTreeItem.vue
+++ b/web_src/js/components/DiffFileTreeItem.vue
@@ -1,5 +1,5 @@
 <script lang="ts" setup>
-import {SvgIcon} from '../svg.ts';
+import {SvgIcon, type SvgName} from '../svg.ts';
 import {diffTreeStore} from '../modules/stores.ts';
 import {ref} from 'vue';
 
@@ -11,7 +11,7 @@ type File = {
   IsSubmodule: boolean;
 }
 
-type Item = {
+export type Item = {
   name: string;
   isFile: boolean;
   file?: File;
@@ -25,18 +25,18 @@ defineProps<{
 const store = diffTreeStore();
 const collapsed = ref(false);
 
-function getIconForDiffType(pType) {
-  const diffTypes = {
-    1: {name: 'octicon-diff-added', classes: ['text', 'green']},
-    2: {name: 'octicon-diff-modified', classes: ['text', 'yellow']},
-    3: {name: 'octicon-diff-removed', classes: ['text', 'red']},
-    4: {name: 'octicon-diff-renamed', classes: ['text', 'teal']},
-    5: {name: 'octicon-diff-renamed', classes: ['text', 'green']}, // there is no octicon for copied, so renamed should be ok
+function getIconForDiffType(pType: number) {
+  const diffTypes: Record<string, {name: SvgName, classes: Array<string>}> = {
+    '1': {name: 'octicon-diff-added', classes: ['text', 'green']},
+    '2': {name: 'octicon-diff-modified', classes: ['text', 'yellow']},
+    '3': {name: 'octicon-diff-removed', classes: ['text', 'red']},
+    '4': {name: 'octicon-diff-renamed', classes: ['text', 'teal']},
+    '5': {name: 'octicon-diff-renamed', classes: ['text', 'green']}, // there is no octicon for copied, so renamed should be ok
   };
-  return diffTypes[pType];
+  return diffTypes[String(pType)];
 }
 
-function fileIcon(file) {
+function fileIcon(file: File) {
   if (file.IsSubmodule) {
     return 'octicon-file-submodule';
   }
diff --git a/web_src/js/components/PullRequestMergeForm.vue b/web_src/js/components/PullRequestMergeForm.vue
index 3be7b802a3..4f291f5ca1 100644
--- a/web_src/js/components/PullRequestMergeForm.vue
+++ b/web_src/js/components/PullRequestMergeForm.vue
@@ -36,17 +36,17 @@ const forceMerge = computed(() => {
 });
 
 watch(mergeStyle, (val) => {
-  mergeStyleDetail.value = mergeForm.value.mergeStyles.find((e) => e.name === val);
+  mergeStyleDetail.value = mergeForm.value.mergeStyles.find((e: any) => e.name === val);
   for (const elem of document.querySelectorAll('[data-pull-merge-style]')) {
     toggleElem(elem, elem.getAttribute('data-pull-merge-style') === val);
   }
 });
 
 onMounted(() => {
-  mergeStyleAllowedCount.value = mergeForm.value.mergeStyles.reduce((v, msd) => v + (msd.allowed ? 1 : 0), 0);
+  mergeStyleAllowedCount.value = mergeForm.value.mergeStyles.reduce((v: any, msd: any) => v + (msd.allowed ? 1 : 0), 0);
 
-  let mergeStyle = mergeForm.value.mergeStyles.find((e) => e.allowed && e.name === mergeForm.value.defaultMergeStyle)?.name;
-  if (!mergeStyle) mergeStyle = mergeForm.value.mergeStyles.find((e) => e.allowed)?.name;
+  let mergeStyle = mergeForm.value.mergeStyles.find((e: any) => e.allowed && e.name === mergeForm.value.defaultMergeStyle)?.name;
+  if (!mergeStyle) mergeStyle = mergeForm.value.mergeStyles.find((e: any) => e.allowed)?.name;
   switchMergeStyle(mergeStyle, !mergeForm.value.canMergeNow);
 
   document.addEventListener('mouseup', hideMergeStyleMenu);
@@ -68,7 +68,7 @@ function toggleActionForm(show: boolean) {
   mergeMessageFieldValue.value = mergeStyleDetail.value.mergeMessageFieldText;
 }
 
-function switchMergeStyle(name, autoMerge = false) {
+function switchMergeStyle(name: string, autoMerge = false) {
   mergeStyle.value = name;
   autoMergeWhenSucceed.value = autoMerge;
 }
diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue
index 914c9e76de..03c8464060 100644
--- a/web_src/js/components/RepoActionView.vue
+++ b/web_src/js/components/RepoActionView.vue
@@ -1,11 +1,12 @@
 <script lang="ts">
 import {SvgIcon} from '../svg.ts';
 import ActionRunStatus from './ActionRunStatus.vue';
-import {createApp} from 'vue';
+import {defineComponent, type PropType} from 'vue';
 import {createElementFromAttrs, toggleElem} from '../utils/dom.ts';
 import {formatDatetime} from '../utils/time.ts';
 import {renderAnsi} from '../render/ansi.ts';
 import {POST, DELETE} from '../modules/fetch.ts';
+import type {IntervalId} from '../types.ts';
 
 // see "models/actions/status.go", if it needs to be used somewhere else, move it to a shared file like "types/actions.ts"
 type RunStatus = 'unknown' | 'waiting' | 'running' | 'success' | 'failure' | 'cancelled' | 'skipped' | 'blocked';
@@ -24,6 +25,20 @@ type LogLineCommand = {
   prefix: string,
 }
 
+type Job = {
+  id: number;
+  name: string;
+  status: RunStatus;
+  canRerun: boolean;
+  duration: string;
+}
+
+type Step = {
+  summary: string,
+  duration: string,
+  status: RunStatus,
+}
+
 function parseLineCommand(line: LogLine): LogLineCommand | null {
   for (const prefix of LogLinePrefixesGroup) {
     if (line.message.startsWith(prefix)) {
@@ -38,7 +53,7 @@ function parseLineCommand(line: LogLine): LogLineCommand | null {
   return null;
 }
 
-function isLogElementInViewport(el: HTMLElement): boolean {
+function isLogElementInViewport(el: Element): boolean {
   const rect = el.getBoundingClientRect();
   return rect.top >= 0 && rect.bottom <= window.innerHeight; // only check height but not width
 }
@@ -57,25 +72,28 @@ function getLocaleStorageOptions(): LocaleStorageOptions {
   return {autoScroll: true, expandRunning: false};
 }
 
-const sfc = {
+export default defineComponent({
   name: 'RepoActionView',
   components: {
     SvgIcon,
     ActionRunStatus,
   },
   props: {
-    runIndex: String,
-    jobIndex: String,
-    actionsURL: String,
-    locale: Object,
-  },
-
-  watch: {
-    optionAlwaysAutoScroll() {
-      this.saveLocaleStorageOptions();
+    runIndex: {
+      type: String,
+      default: '',
     },
-    optionAlwaysExpandRunning() {
-      this.saveLocaleStorageOptions();
+    jobIndex: {
+      type: String,
+      default: '',
+    },
+    actionsURL: {
+      type: String,
+      default: '',
+    },
+    locale: {
+      type: Object as PropType<Record<string, any>>,
+      default: null,
     },
   },
 
@@ -83,10 +101,10 @@ const sfc = {
     const {autoScroll, expandRunning} = getLocaleStorageOptions();
     return {
       // internal state
-      loadingAbortController: null,
-      intervalID: null,
-      currentJobStepsStates: [],
-      artifacts: [],
+      loadingAbortController: null as AbortController | null,
+      intervalID: null as IntervalId | null,
+      currentJobStepsStates: [] as Array<Record<string, any>>,
+      artifacts: [] as Array<Record<string, any>>,
       onHoverRerunIndex: -1,
       menuVisible: false,
       isFullScreen: false,
@@ -102,10 +120,11 @@ const sfc = {
         link: '',
         title: '',
         titleHTML: '',
-        status: '',
+        status: 'unknown' as RunStatus,
         canCancel: false,
         canApprove: false,
         canRerun: false,
+        canDeleteArtifact: false,
         done: false,
         workflowID: '',
         workflowLink: '',
@@ -118,7 +137,7 @@ const sfc = {
           //   canRerun: false,
           //   duration: '',
           // },
-        ],
+        ] as Array<Job>,
         commit: {
           localeCommit: '',
           localePushedBy: '',
@@ -131,6 +150,7 @@ const sfc = {
           branch: {
             name: '',
             link: '',
+            isDeleted: false,
           },
         },
       },
@@ -143,12 +163,21 @@ const sfc = {
           //   duration: '',
           //   status: '',
           // }
-        ],
+        ] as Array<Step>,
       },
     };
   },
 
-  async mounted() {
+  watch: {
+    optionAlwaysAutoScroll() {
+      this.saveLocaleStorageOptions();
+    },
+    optionAlwaysExpandRunning() {
+      this.saveLocaleStorageOptions();
+    },
+  },
+
+  async mounted() { // eslint-disable-line @typescript-eslint/no-misused-promises
     // load job data and then auto-reload periodically
     // need to await first loadJob so this.currentJobStepsStates is initialized and can be used in hashChangeListener
     await this.loadJob();
@@ -180,17 +209,18 @@ const sfc = {
 
     // get the job step logs container ('.job-step-logs')
     getJobStepLogsContainer(stepIndex: number): HTMLElement {
-      return this.$refs.logs[stepIndex];
+      return (this.$refs.logs as any)[stepIndex];
     },
 
     // get the active logs container element, either the `job-step-logs` or the `job-log-list` in the `job-log-group`
     getActiveLogsContainer(stepIndex: number): HTMLElement {
       const el = this.getJobStepLogsContainer(stepIndex);
+      // @ts-expect-error - _stepLogsActiveContainer is a custom property
       return el._stepLogsActiveContainer ?? el;
     },
     // begin a log group
     beginLogGroup(stepIndex: number, startTime: number, line: LogLine, cmd: LogLineCommand) {
-      const el = this.$refs.logs[stepIndex];
+      const el = (this.$refs.logs as any)[stepIndex];
       const elJobLogGroupSummary = createElementFromAttrs('summary', {class: 'job-log-group-summary'},
         this.createLogLine(stepIndex, startTime, {
           index: line.index,
@@ -208,7 +238,7 @@ const sfc = {
     },
     // end a log group
     endLogGroup(stepIndex: number, startTime: number, line: LogLine, cmd: LogLineCommand) {
-      const el = this.$refs.logs[stepIndex];
+      const el = (this.$refs.logs as any)[stepIndex];
       el._stepLogsActiveContainer = null;
       el.append(this.createLogLine(stepIndex, startTime, {
         index: line.index,
@@ -263,7 +293,7 @@ const sfc = {
       const el = this.getJobStepLogsContainer(stepIndex);
       // if the logs container is empty, then auto-scroll if the step is expanded
       if (!el.lastChild) return this.currentJobStepsStates[stepIndex].expanded;
-      return isLogElementInViewport(el.lastChild);
+      return isLogElementInViewport(el.lastChild as Element);
     },
 
     appendLogs(stepIndex: number, startTime: number, logLines: LogLine[]) {
@@ -378,9 +408,9 @@ const sfc = {
       if (this.menuVisible) this.menuVisible = false;
     },
 
-    toggleTimeDisplay(type: string) {
+    toggleTimeDisplay(type: 'seconds' | 'stamp') {
       this.timeVisible[`log-time-${type}`] = !this.timeVisible[`log-time-${type}`];
-      for (const el of this.$refs.steps.querySelectorAll(`.log-time-${type}`)) {
+      for (const el of (this.$refs.steps as HTMLElement).querySelectorAll(`.log-time-${type}`)) {
         toggleElem(el, this.timeVisible[`log-time-${type}`]);
       }
     },
@@ -407,66 +437,20 @@ const sfc = {
       const selectedLogStep = window.location.hash;
       if (!selectedLogStep) return;
       const [_, step, _line] = selectedLogStep.split('-');
-      if (!this.currentJobStepsStates[step]) return;
-      if (!this.currentJobStepsStates[step].expanded && this.currentJobStepsStates[step].cursor === null) {
-        this.currentJobStepsStates[step].expanded = true;
+      const stepNum = Number(step);
+      if (!this.currentJobStepsStates[stepNum]) return;
+      if (!this.currentJobStepsStates[stepNum].expanded && this.currentJobStepsStates[stepNum].cursor === null) {
+        this.currentJobStepsStates[stepNum].expanded = true;
         // need to await for load job if the step log is loaded for the first time
         // so logline can be selected by querySelector
         await this.loadJob();
       }
-      const logLine = this.$refs.steps.querySelector(selectedLogStep);
+      const logLine = (this.$refs.steps as HTMLElement).querySelector(selectedLogStep);
       if (!logLine) return;
-      logLine.querySelector('.line-num').click();
+      logLine.querySelector<HTMLAnchorElement>('.line-num').click();
     },
   },
-};
-
-export default sfc;
-
-export function initRepositoryActionView() {
-  const el = document.querySelector('#repo-action-view');
-  if (!el) return;
-
-  // TODO: the parent element's full height doesn't work well now,
-  // but we can not pollute the global style at the moment, only fix the height problem for pages with this component
-  const parentFullHeight = document.querySelector<HTMLElement>('body > div.full.height');
-  if (parentFullHeight) parentFullHeight.style.paddingBottom = '0';
-
-  const view = createApp(sfc, {
-    runIndex: el.getAttribute('data-run-index'),
-    jobIndex: el.getAttribute('data-job-index'),
-    actionsURL: el.getAttribute('data-actions-url'),
-    locale: {
-      approve: el.getAttribute('data-locale-approve'),
-      cancel: el.getAttribute('data-locale-cancel'),
-      rerun: el.getAttribute('data-locale-rerun'),
-      rerun_all: el.getAttribute('data-locale-rerun-all'),
-      scheduled: el.getAttribute('data-locale-runs-scheduled'),
-      commit: el.getAttribute('data-locale-runs-commit'),
-      pushedBy: el.getAttribute('data-locale-runs-pushed-by'),
-      artifactsTitle: el.getAttribute('data-locale-artifacts-title'),
-      areYouSure: el.getAttribute('data-locale-are-you-sure'),
-      confirmDeleteArtifact: el.getAttribute('data-locale-confirm-delete-artifact'),
-      showTimeStamps: el.getAttribute('data-locale-show-timestamps'),
-      showLogSeconds: el.getAttribute('data-locale-show-log-seconds'),
-      showFullScreen: el.getAttribute('data-locale-show-full-screen'),
-      downloadLogs: el.getAttribute('data-locale-download-logs'),
-      status: {
-        unknown: el.getAttribute('data-locale-status-unknown'),
-        waiting: el.getAttribute('data-locale-status-waiting'),
-        running: el.getAttribute('data-locale-status-running'),
-        success: el.getAttribute('data-locale-status-success'),
-        failure: el.getAttribute('data-locale-status-failure'),
-        cancelled: el.getAttribute('data-locale-status-cancelled'),
-        skipped: el.getAttribute('data-locale-status-skipped'),
-        blocked: el.getAttribute('data-locale-status-blocked'),
-      },
-      logsAlwaysAutoScroll: el.getAttribute('data-locale-logs-always-auto-scroll'),
-      logsAlwaysExpandRunning: el.getAttribute('data-locale-logs-always-expand-running'),
-    },
-  });
-  view.mount(el);
-}
+});
 </script>
 <template>
   <div class="ui container action-view-container">
diff --git a/web_src/js/components/RepoActivityTopAuthors.vue b/web_src/js/components/RepoActivityTopAuthors.vue
index 1f5e9825ba..77b85bd7e2 100644
--- a/web_src/js/components/RepoActivityTopAuthors.vue
+++ b/web_src/js/components/RepoActivityTopAuthors.vue
@@ -1,4 +1,5 @@
 <script lang="ts" setup>
+// @ts-expect-error - module exports no types
 import {VueBarGraph} from 'vue-bar-graph';
 import {computed, onMounted, ref} from 'vue';
 
@@ -8,13 +9,15 @@ const colors = ref({
   textAltColor: 'white',
 });
 
-// possible keys:
-// * avatar_link: (...)
-// * commits: (...)
-// * home_link: (...)
-// * login: (...)
-// * name: (...)
-const activityTopAuthors = window.config.pageData.repoActivityTopAuthors || [];
+type ActivityAuthorData = {
+  avatar_link: string;
+  commits: number;
+  home_link: string;
+  login: string;
+  name: string;
+}
+
+const activityTopAuthors: Array<ActivityAuthorData> = window.config.pageData.repoActivityTopAuthors || [];
 
 const graphPoints = computed(() => {
   return activityTopAuthors.map((item) => {
@@ -26,7 +29,7 @@ const graphPoints = computed(() => {
 });
 
 const graphAuthors = computed(() => {
-  return activityTopAuthors.map((item, idx) => {
+  return activityTopAuthors.map((item, idx: number) => {
     return {
       position: idx + 1,
       ...item,
diff --git a/web_src/js/components/RepoBranchTagSelector.vue b/web_src/js/components/RepoBranchTagSelector.vue
index a5ed8b6dad..fa5c75af99 100644
--- a/web_src/js/components/RepoBranchTagSelector.vue
+++ b/web_src/js/components/RepoBranchTagSelector.vue
@@ -1,5 +1,5 @@
 <script lang="ts">
-import {nextTick} from 'vue';
+import {defineComponent, nextTick} from 'vue';
 import {SvgIcon} from '../svg.ts';
 import {showErrorToast} from '../modules/toast.ts';
 import {GET} from '../modules/fetch.ts';
@@ -17,51 +17,11 @@ type SelectedTab = 'branches' | 'tags';
 
 type TabLoadingStates = Record<SelectedTab, '' | 'loading' | 'done'>
 
-const sfc = {
+export default defineComponent({
   components: {SvgIcon},
   props: {
     elRoot: HTMLElement,
   },
-  computed: {
-    searchFieldPlaceholder() {
-      return this.selectedTab === 'branches' ? this.textFilterBranch : this.textFilterTag;
-    },
-    filteredItems(): ListItem[] {
-      const searchTermLower = this.searchTerm.toLowerCase();
-      const items = this.allItems.filter((item: ListItem) => {
-        const typeMatched = (this.selectedTab === 'branches' && item.refType === 'branch') || (this.selectedTab === 'tags' && item.refType === 'tag');
-        if (!typeMatched) return false;
-        if (!this.searchTerm) return true; // match all
-        return item.refShortName.toLowerCase().includes(searchTermLower);
-      });
-
-      // TODO: fix this anti-pattern: side-effects-in-computed-properties
-      this.activeItemIndex = !items.length && this.showCreateNewRef ? 0 : -1;
-      return items;
-    },
-    showNoResults() {
-      if (this.tabLoadingStates[this.selectedTab] !== 'done') return false;
-      return !this.filteredItems.length && !this.showCreateNewRef;
-    },
-    showCreateNewRef() {
-      if (!this.allowCreateNewRef || !this.searchTerm) {
-        return false;
-      }
-      return !this.allItems.filter((item: ListItem) => {
-        return item.refShortName === this.searchTerm; // FIXME: not quite right here, it mixes "branch" and "tag" names
-      }).length;
-    },
-    createNewRefFormActionUrl() {
-      return `${this.currentRepoLink}/branches/_new/${this.currentRefType}/${pathEscapeSegments(this.currentRefShortName)}`;
-    },
-  },
-  watch: {
-    menuVisible(visible: boolean) {
-      if (!visible) return;
-      this.focusSearchField();
-      this.loadTabItems();
-    },
-  },
   data() {
     const shouldShowTabBranches = this.elRoot.getAttribute('data-show-tab-branches') === 'true';
     return {
@@ -89,7 +49,7 @@ const sfc = {
       currentRepoDefaultBranch: this.elRoot.getAttribute('data-current-repo-default-branch'),
       currentRepoLink: this.elRoot.getAttribute('data-current-repo-link'),
       currentTreePath: this.elRoot.getAttribute('data-current-tree-path'),
-      currentRefType: this.elRoot.getAttribute('data-current-ref-type'),
+      currentRefType: this.elRoot.getAttribute('data-current-ref-type') as GitRefType,
       currentRefShortName: this.elRoot.getAttribute('data-current-ref-short-name'),
 
       refLinkTemplate: this.elRoot.getAttribute('data-ref-link-template'),
@@ -102,6 +62,46 @@ const sfc = {
       enableFeed: this.elRoot.getAttribute('data-enable-feed') === 'true',
     };
   },
+  computed: {
+    searchFieldPlaceholder() {
+      return this.selectedTab === 'branches' ? this.textFilterBranch : this.textFilterTag;
+    },
+    filteredItems(): ListItem[] {
+      const searchTermLower = this.searchTerm.toLowerCase();
+      const items = this.allItems.filter((item: ListItem) => {
+        const typeMatched = (this.selectedTab === 'branches' && item.refType === 'branch') || (this.selectedTab === 'tags' && item.refType === 'tag');
+        if (!typeMatched) return false;
+        if (!this.searchTerm) return true; // match all
+        return item.refShortName.toLowerCase().includes(searchTermLower);
+      });
+
+      // TODO: fix this anti-pattern: side-effects-in-computed-properties
+      this.activeItemIndex = !items.length && this.showCreateNewRef ? 0 : -1; // eslint-disable-line vue/no-side-effects-in-computed-properties
+      return items;
+    },
+    showNoResults() {
+      if (this.tabLoadingStates[this.selectedTab] !== 'done') return false;
+      return !this.filteredItems.length && !this.showCreateNewRef;
+    },
+    showCreateNewRef() {
+      if (!this.allowCreateNewRef || !this.searchTerm) {
+        return false;
+      }
+      return !this.allItems.filter((item: ListItem) => {
+        return item.refShortName === this.searchTerm; // FIXME: not quite right here, it mixes "branch" and "tag" names
+      }).length;
+    },
+    createNewRefFormActionUrl() {
+      return `${this.currentRepoLink}/branches/_new/${this.currentRefType}/${pathEscapeSegments(this.currentRefShortName)}`;
+    },
+  },
+  watch: {
+    menuVisible(visible: boolean) {
+      if (!visible) return;
+      this.focusSearchField();
+      this.loadTabItems();
+    },
+  },
   beforeMount() {
     document.body.addEventListener('click', (e) => {
       if (this.$el.contains(e.target)) return;
@@ -139,11 +139,11 @@ const sfc = {
       }
     },
     createNewRef() {
-      this.$refs.createNewRefForm?.submit();
+      (this.$refs.createNewRefForm as HTMLFormElement)?.submit();
     },
     focusSearchField() {
       nextTick(() => {
-        this.$refs.searchField.focus();
+        (this.$refs.searchField as HTMLInputElement).focus();
       });
     },
     getSelectedIndexInFiltered() {
@@ -154,9 +154,10 @@ const sfc = {
     },
     getActiveItem() {
       const el = this.$refs[`listItem${this.activeItemIndex}`]; // eslint-disable-line no-jquery/variable-pattern
+      // @ts-expect-error - el is unknown type
       return (el && el.length) ? el[0] : null;
     },
-    keydown(e) {
+    keydown(e: KeyboardEvent) {
       if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
         e.preventDefault();
 
@@ -180,7 +181,7 @@ const sfc = {
         this.menuVisible = false;
       }
     },
-    handleTabSwitch(selectedTab) {
+    handleTabSwitch(selectedTab: SelectedTab) {
       this.selectedTab = selectedTab;
       this.focusSearchField();
       this.loadTabItems();
@@ -212,9 +213,7 @@ const sfc = {
       }
     },
   },
-};
-
-export default sfc; // activate IDE's Vue plugin
+});
 </script>
 <template>
   <div class="ui dropdown custom branch-selector-dropdown ellipsis-items-nowrap">
diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue
index e79fc80d8e..6ad2c848b1 100644
--- a/web_src/js/components/RepoContributors.vue
+++ b/web_src/js/components/RepoContributors.vue
@@ -1,4 +1,5 @@
 <script lang="ts">
+import {defineComponent, type PropType} from 'vue';
 import {SvgIcon} from '../svg.ts';
 import dayjs from 'dayjs';
 import {
@@ -56,11 +57,11 @@ Chart.register(
   customEventListener,
 );
 
-export default {
+export default defineComponent({
   components: {ChartLine, SvgIcon},
   props: {
     locale: {
-      type: Object,
+      type: Object as PropType<Record<string, any>>,
       required: true,
     },
     repoLink: {
@@ -79,16 +80,16 @@ export default {
     sortedContributors: {} as Record<string, any>,
     type: 'commits',
     contributorsStats: {} as Record<string, any>,
-    xAxisStart: null,
-    xAxisEnd: null,
-    xAxisMin: null,
-    xAxisMax: null,
+    xAxisStart: null as number | null,
+    xAxisEnd: null as number | null,
+    xAxisMin: null as number | null,
+    xAxisMax: null as number | null,
   }),
   mounted() {
     this.fetchGraphData();
 
     fomanticQuery('#repo-contributors').dropdown({
-      onChange: (val) => {
+      onChange: (val: string) => {
         this.xAxisMin = this.xAxisStart;
         this.xAxisMax = this.xAxisEnd;
         this.type = val;
@@ -98,7 +99,7 @@ export default {
   },
   methods: {
     sortContributors() {
-      const contributors = this.filterContributorWeeksByDateRange();
+      const contributors: Record<string, any> = this.filterContributorWeeksByDateRange();
       const criteria = `total_${this.type}`;
       this.sortedContributors = Object.values(contributors)
         .filter((contributor) => contributor[criteria] !== 0)
@@ -157,7 +158,7 @@ export default {
     },
 
     filterContributorWeeksByDateRange() {
-      const filteredData = {};
+      const filteredData: Record<string, any> = {};
       const data = this.contributorsStats;
       for (const key of Object.keys(data)) {
         const user = data[key];
@@ -195,7 +196,7 @@ export default {
       // Normally, chartjs handles this automatically, but it will resize the graph when you
       // zoom, pan etc. I think resizing the graph makes it harder to compare things visually.
       const maxValue = Math.max(
-        ...this.totalStats.weeks.map((o) => o[this.type]),
+        ...this.totalStats.weeks.map((o: Record<string, any>) => o[this.type]),
       );
       const [coefficient, exp] = maxValue.toExponential().split('e').map(Number);
       if (coefficient % 1 === 0) return maxValue;
@@ -207,7 +208,7 @@ export default {
       // for contributors' graph. If I let chartjs do this for me, it will choose different
       // maxY value for each contributors' graph which again makes it harder to compare.
       const maxValue = Math.max(
-        ...this.sortedContributors.map((c) => c.max_contribution_type),
+        ...this.sortedContributors.map((c: Record<string, any>) => c.max_contribution_type),
       );
       const [coefficient, exp] = maxValue.toExponential().split('e').map(Number);
       if (coefficient % 1 === 0) return maxValue;
@@ -231,8 +232,8 @@ export default {
     },
 
     updateOtherCharts({chart}: {chart: Chart}, reset: boolean = false) {
-      const minVal = chart.options.scales.x.min;
-      const maxVal = chart.options.scales.x.max;
+      const minVal = Number(chart.options.scales.x.min);
+      const maxVal = Number(chart.options.scales.x.max);
       if (reset) {
         this.xAxisMin = this.xAxisStart;
         this.xAxisMax = this.xAxisEnd;
@@ -320,7 +321,7 @@ export default {
       };
     },
   },
-};
+});
 </script>
 <template>
   <div>
diff --git a/web_src/js/components/ScopedAccessTokenSelector.vue b/web_src/js/components/ScopedAccessTokenSelector.vue
index 63214d0bf5..9eaf824035 100644
--- a/web_src/js/components/ScopedAccessTokenSelector.vue
+++ b/web_src/js/components/ScopedAccessTokenSelector.vue
@@ -35,7 +35,7 @@ onUnmounted(() => {
   document.querySelector('#scoped-access-submit').removeEventListener('click', onClickSubmit);
 });
 
-function onClickSubmit(e) {
+function onClickSubmit(e: Event) {
   e.preventDefault();
 
   const warningEl = document.querySelector('#scoped-access-warning');
diff --git a/web_src/js/features/admin/common.ts b/web_src/js/features/admin/common.ts
index 6c725a3efe..b991749d81 100644
--- a/web_src/js/features/admin/common.ts
+++ b/web_src/js/features/admin/common.ts
@@ -90,7 +90,7 @@ export function initAdminCommon(): void {
     onOAuth2UseCustomURLChange(applyDefaultValues);
   }
 
-  function onOAuth2UseCustomURLChange(applyDefaultValues) {
+  function onOAuth2UseCustomURLChange(applyDefaultValues: boolean) {
     const provider = document.querySelector<HTMLInputElement>('#oauth2_provider').value;
     hideElem('.oauth2_use_custom_url_field');
     for (const input of document.querySelectorAll<HTMLInputElement>('.oauth2_use_custom_url_field input[required]')) {
diff --git a/web_src/js/features/citation.ts b/web_src/js/features/citation.ts
index fc5bb38f0a..3c9fe0afc8 100644
--- a/web_src/js/features/citation.ts
+++ b/web_src/js/features/citation.ts
@@ -5,9 +5,13 @@ const {pageData} = window.config;
 
 async function initInputCitationValue(citationCopyApa: HTMLButtonElement, citationCopyBibtex: HTMLButtonElement) {
   const [{Cite, plugins}] = await Promise.all([
+    // @ts-expect-error: module exports no types
     import(/* webpackChunkName: "citation-js-core" */'@citation-js/core'),
+    // @ts-expect-error: module exports no types
     import(/* webpackChunkName: "citation-js-formats" */'@citation-js/plugin-software-formats'),
+    // @ts-expect-error: module exports no types
     import(/* webpackChunkName: "citation-js-bibtex" */'@citation-js/plugin-bibtex'),
+    // @ts-expect-error: module exports no types
     import(/* webpackChunkName: "citation-js-csl" */'@citation-js/plugin-csl'),
   ]);
   const {citationFileContent} = pageData;
diff --git a/web_src/js/features/common-button.ts b/web_src/js/features/common-button.ts
index 3162557b9b..7aebdd8dd5 100644
--- a/web_src/js/features/common-button.ts
+++ b/web_src/js/features/common-button.ts
@@ -74,10 +74,10 @@ export function initGlobalDeleteButton(): void {
   }
 }
 
-function onShowPanelClick(e) {
+function onShowPanelClick(e: MouseEvent) {
   // a '.show-panel' element can show a panel, by `data-panel="selector"`
   // if it has "toggle" class, it toggles the panel
-  const el = e.currentTarget;
+  const el = e.currentTarget as HTMLElement;
   e.preventDefault();
   const sel = el.getAttribute('data-panel');
   if (el.classList.contains('toggle')) {
@@ -87,9 +87,9 @@ function onShowPanelClick(e) {
   }
 }
 
-function onHidePanelClick(e) {
+function onHidePanelClick(e: MouseEvent) {
   // a `.hide-panel` element can hide a panel, by `data-panel="selector"` or `data-panel-closest="selector"`
-  const el = e.currentTarget;
+  const el = e.currentTarget as HTMLElement;
   e.preventDefault();
   let sel = el.getAttribute('data-panel');
   if (sel) {
@@ -98,13 +98,13 @@ function onHidePanelClick(e) {
   }
   sel = el.getAttribute('data-panel-closest');
   if (sel) {
-    hideElem(el.parentNode.closest(sel));
+    hideElem((el.parentNode as HTMLElement).closest(sel));
     return;
   }
   throw new Error('no panel to hide'); // should never happen, otherwise there is a bug in code
 }
 
-function onShowModalClick(e) {
+function onShowModalClick(e: MouseEvent) {
   // A ".show-modal" button will show a modal dialog defined by its "data-modal" attribute.
   // Each "data-modal-{target}" attribute will be filled to target element's value or text-content.
   // * First, try to query '#target'
@@ -112,7 +112,7 @@ function onShowModalClick(e) {
   // * Then, try to query '.target'
   // * Then, try to query 'target' as HTML tag
   // If there is a ".{attr}" part like "data-modal-form.action", then the form's "action" attribute will be set.
-  const el = e.currentTarget;
+  const el = e.currentTarget as HTMLElement;
   e.preventDefault();
   const modalSelector = el.getAttribute('data-modal');
   const elModal = document.querySelector(modalSelector);
@@ -137,9 +137,9 @@ function onShowModalClick(e) {
     }
 
     if (attrTargetAttr) {
-      attrTarget[camelize(attrTargetAttr)] = attrib.value;
+      (attrTarget as any)[camelize(attrTargetAttr)] = attrib.value;
     } else if (attrTarget.matches('input, textarea')) {
-      attrTarget.value = attrib.value; // FIXME: add more supports like checkbox
+      (attrTarget as HTMLInputElement | HTMLTextAreaElement).value = attrib.value; // FIXME: add more supports like checkbox
     } else {
       attrTarget.textContent = attrib.value; // FIXME: it should be more strict here, only handle div/span/p
     }
diff --git a/web_src/js/features/common-fetch-action.ts b/web_src/js/features/common-fetch-action.ts
index bc72f4089a..2da481e521 100644
--- a/web_src/js/features/common-fetch-action.ts
+++ b/web_src/js/features/common-fetch-action.ts
@@ -75,7 +75,10 @@ async function formFetchAction(formEl: HTMLFormElement, e: SubmitEvent) {
   }
 
   let reqUrl = formActionUrl;
-  const reqOpt = {method: formMethod.toUpperCase(), body: null};
+  const reqOpt = {
+    method: formMethod.toUpperCase(),
+    body: null as FormData | null,
+  };
   if (formMethod.toLowerCase() === 'get') {
     const params = new URLSearchParams();
     for (const [key, value] of formData) {
diff --git a/web_src/js/features/common-form.ts b/web_src/js/features/common-form.ts
index 8532d397cd..7321d80c44 100644
--- a/web_src/js/features/common-form.ts
+++ b/web_src/js/features/common-form.ts
@@ -17,13 +17,13 @@ export function initGlobalEnterQuickSubmit() {
     if (e.key !== 'Enter') return;
     const hasCtrlOrMeta = ((e.ctrlKey || e.metaKey) && !e.altKey);
     if (hasCtrlOrMeta && e.target.matches('textarea')) {
-      if (handleGlobalEnterQuickSubmit(e.target)) {
+      if (handleGlobalEnterQuickSubmit(e.target as HTMLElement)) {
         e.preventDefault();
       }
     } else if (e.target.matches('input') && !e.target.closest('form')) {
       // input in a normal form could handle Enter key by default, so we only handle the input outside a form
       // eslint-disable-next-line unicorn/no-lonely-if
-      if (handleGlobalEnterQuickSubmit(e.target)) {
+      if (handleGlobalEnterQuickSubmit(e.target as HTMLElement)) {
         e.preventDefault();
       }
     }
diff --git a/web_src/js/features/common-organization.ts b/web_src/js/features/common-organization.ts
index 47a61ece22..a1f19bedea 100644
--- a/web_src/js/features/common-organization.ts
+++ b/web_src/js/features/common-organization.ts
@@ -6,7 +6,7 @@ export function initCommonOrganization() {
     return;
   }
 
-  document.querySelector('.organization.settings.options #org_name')?.addEventListener('input', function () {
+  document.querySelector<HTMLInputElement>('.organization.settings.options #org_name')?.addEventListener('input', function () {
     const nameChanged = this.value.toLowerCase() !== this.getAttribute('data-org-name').toLowerCase();
     toggleElem('#org-name-change-prompt', nameChanged);
   });
diff --git a/web_src/js/features/comp/ComboMarkdownEditor.ts b/web_src/js/features/comp/ComboMarkdownEditor.ts
index bba50a1296..d3773a89c4 100644
--- a/web_src/js/features/comp/ComboMarkdownEditor.ts
+++ b/web_src/js/features/comp/ComboMarkdownEditor.ts
@@ -29,10 +29,10 @@ let elementIdCounter = 0;
 
 /**
  * validate if the given textarea is non-empty.
- * @param {HTMLElement} textarea - The textarea element to be validated.
+ * @param {HTMLTextAreaElement} textarea - The textarea element to be validated.
  * @returns {boolean} returns true if validation succeeded.
  */
-export function validateTextareaNonEmpty(textarea) {
+export function validateTextareaNonEmpty(textarea: HTMLTextAreaElement) {
   // When using EasyMDE, the original edit area HTML element is hidden, breaking HTML5 input validation.
   // The workaround (https://github.com/sparksuite/simplemde-markdown-editor/issues/324) doesn't work with contenteditable, so we just show an alert.
   if (!textarea.value) {
@@ -49,16 +49,25 @@ export function validateTextareaNonEmpty(textarea) {
   return true;
 }
 
+type Heights = {
+  minHeight?: string,
+  height?: string,
+  maxHeight?: string,
+};
+
 type ComboMarkdownEditorOptions = {
-  editorHeights?: {minHeight?: string, height?: string, maxHeight?: string},
+  editorHeights?: Heights,
   easyMDEOptions?: EasyMDE.Options,
 };
 
+type ComboMarkdownEditorTextarea = HTMLTextAreaElement & {_giteaComboMarkdownEditor: any};
+type ComboMarkdownEditorContainer = HTMLElement & {_giteaComboMarkdownEditor?: any};
+
 export class ComboMarkdownEditor {
   static EventEditorContentChanged = EventEditorContentChanged;
   static EventUploadStateChanged = EventUploadStateChanged;
 
-  public container : HTMLElement;
+  public container: HTMLElement;
 
   options: ComboMarkdownEditorOptions;
 
@@ -70,7 +79,7 @@ export class ComboMarkdownEditor {
   easyMDEToolbarActions: any;
   easyMDEToolbarDefault: any;
 
-  textarea: HTMLTextAreaElement & {_giteaComboMarkdownEditor: any};
+  textarea: ComboMarkdownEditorTextarea;
   textareaMarkdownToolbar: HTMLElement;
   textareaAutosize: any;
 
@@ -81,7 +90,7 @@ export class ComboMarkdownEditor {
   previewUrl: string;
   previewContext: string;
 
-  constructor(container, options:ComboMarkdownEditorOptions = {}) {
+  constructor(container: ComboMarkdownEditorContainer, options:ComboMarkdownEditorOptions = {}) {
     if (container._giteaComboMarkdownEditor) throw new Error('ComboMarkdownEditor already initialized');
     container._giteaComboMarkdownEditor = this;
     this.options = options;
@@ -98,7 +107,7 @@ export class ComboMarkdownEditor {
     await this.switchToUserPreference();
   }
 
-  applyEditorHeights(el, heights) {
+  applyEditorHeights(el: HTMLElement, heights: Heights) {
     if (!heights) return;
     if (heights.minHeight) el.style.minHeight = heights.minHeight;
     if (heights.height) el.style.height = heights.height;
@@ -283,7 +292,7 @@ export class ComboMarkdownEditor {
     ];
   }
 
-  parseEasyMDEToolbar(easyMde: typeof EasyMDE, actions) {
+  parseEasyMDEToolbar(easyMde: typeof EasyMDE, actions: any) {
     this.easyMDEToolbarActions = this.easyMDEToolbarActions || easyMDEToolbarActions(easyMde, this);
     const processed = [];
     for (const action of actions) {
@@ -332,21 +341,21 @@ export class ComboMarkdownEditor {
     this.easyMDE = new EasyMDE(easyMDEOpt);
     this.easyMDE.codemirror.on('change', () => triggerEditorContentChanged(this.container));
     this.easyMDE.codemirror.setOption('extraKeys', {
-      'Cmd-Enter': (cm) => handleGlobalEnterQuickSubmit(cm.getTextArea()),
-      'Ctrl-Enter': (cm) => handleGlobalEnterQuickSubmit(cm.getTextArea()),
-      Enter: (cm) => {
+      'Cmd-Enter': (cm: any) => handleGlobalEnterQuickSubmit(cm.getTextArea()),
+      'Ctrl-Enter': (cm: any) => handleGlobalEnterQuickSubmit(cm.getTextArea()),
+      Enter: (cm: any) => {
         const tributeContainer = document.querySelector<HTMLElement>('.tribute-container');
         if (!tributeContainer || tributeContainer.style.display === 'none') {
           cm.execCommand('newlineAndIndent');
         }
       },
-      Up: (cm) => {
+      Up: (cm: any) => {
         const tributeContainer = document.querySelector<HTMLElement>('.tribute-container');
         if (!tributeContainer || tributeContainer.style.display === 'none') {
           return cm.execCommand('goLineUp');
         }
       },
-      Down: (cm) => {
+      Down: (cm: any) => {
         const tributeContainer = document.querySelector<HTMLElement>('.tribute-container');
         if (!tributeContainer || tributeContainer.style.display === 'none') {
           return cm.execCommand('goLineDown');
@@ -354,14 +363,14 @@ export class ComboMarkdownEditor {
       },
     });
     this.applyEditorHeights(this.container.querySelector('.CodeMirror-scroll'), this.options.editorHeights);
-    await attachTribute(this.easyMDE.codemirror.getInputField(), {mentions: true, emoji: true});
+    await attachTribute(this.easyMDE.codemirror.getInputField());
     if (this.dropzone) {
       initEasyMDEPaste(this.easyMDE, this.dropzone);
     }
     hideElem(this.textareaMarkdownToolbar);
   }
 
-  value(v = undefined) {
+  value(v: any = undefined) {
     if (v === undefined) {
       if (this.easyMDE) {
         return this.easyMDE.value();
@@ -402,7 +411,7 @@ export class ComboMarkdownEditor {
   }
 }
 
-export function getComboMarkdownEditor(el) {
+export function getComboMarkdownEditor(el: any) {
   if (!el) return null;
   if (el.length) el = el[0];
   return el._giteaComboMarkdownEditor;
diff --git a/web_src/js/features/comp/EditorMarkdown.ts b/web_src/js/features/comp/EditorMarkdown.ts
index d3ed492396..6e66c15763 100644
--- a/web_src/js/features/comp/EditorMarkdown.ts
+++ b/web_src/js/features/comp/EditorMarkdown.ts
@@ -1,10 +1,10 @@
 export const EventEditorContentChanged = 'ce-editor-content-changed';
 
-export function triggerEditorContentChanged(target) {
+export function triggerEditorContentChanged(target: HTMLElement) {
   target.dispatchEvent(new CustomEvent(EventEditorContentChanged, {bubbles: true}));
 }
 
-export function textareaInsertText(textarea, value) {
+export function textareaInsertText(textarea: HTMLTextAreaElement, value: string) {
   const startPos = textarea.selectionStart;
   const endPos = textarea.selectionEnd;
   textarea.value = textarea.value.substring(0, startPos) + value + textarea.value.substring(endPos);
@@ -20,7 +20,7 @@ type TextareaValueSelection = {
   selEnd: number;
 }
 
-function handleIndentSelection(textarea: HTMLTextAreaElement, e) {
+function handleIndentSelection(textarea: HTMLTextAreaElement, e: KeyboardEvent) {
   const selStart = textarea.selectionStart;
   const selEnd = textarea.selectionEnd;
   if (selEnd === selStart) return; // do not process when no selection
@@ -184,8 +184,13 @@ function handleNewline(textarea: HTMLTextAreaElement, e: Event) {
   triggerEditorContentChanged(textarea);
 }
 
-export function initTextareaMarkdown(textarea) {
+function isTextExpanderShown(textarea: HTMLElement): boolean {
+  return Boolean(textarea.closest('text-expander')?.querySelector('.suggestions'));
+}
+
+export function initTextareaMarkdown(textarea: HTMLTextAreaElement) {
   textarea.addEventListener('keydown', (e) => {
+    if (isTextExpanderShown(textarea)) return;
     if (e.key === 'Tab' && !e.ctrlKey && !e.metaKey && !e.altKey) {
       // use Tab/Shift-Tab to indent/unindent the selected lines
       handleIndentSelection(textarea, e);
diff --git a/web_src/js/features/comp/EditorUpload.ts b/web_src/js/features/comp/EditorUpload.ts
index 89982747ea..f6d5731422 100644
--- a/web_src/js/features/comp/EditorUpload.ts
+++ b/web_src/js/features/comp/EditorUpload.ts
@@ -8,43 +8,46 @@ import {
   generateMarkdownLinkForAttachment,
 } from '../dropzone.ts';
 import type CodeMirror from 'codemirror';
+import type EasyMDE from 'easymde';
+import type {DropzoneFile} from 'dropzone';
 
 let uploadIdCounter = 0;
 
 export const EventUploadStateChanged = 'ce-upload-state-changed';
 
-export function triggerUploadStateChanged(target) {
+export function triggerUploadStateChanged(target: HTMLElement) {
   target.dispatchEvent(new CustomEvent(EventUploadStateChanged, {bubbles: true}));
 }
 
-function uploadFile(dropzoneEl, file) {
+function uploadFile(dropzoneEl: HTMLElement, file: File) {
   return new Promise((resolve) => {
     const curUploadId = uploadIdCounter++;
-    file._giteaUploadId = curUploadId;
+    (file as any)._giteaUploadId = curUploadId;
     const dropzoneInst = dropzoneEl.dropzone;
-    const onUploadDone = ({file}) => {
+    const onUploadDone = ({file}: {file: any}) => {
       if (file._giteaUploadId === curUploadId) {
         dropzoneInst.off(DropzoneCustomEventUploadDone, onUploadDone);
         resolve(file);
       }
     };
     dropzoneInst.on(DropzoneCustomEventUploadDone, onUploadDone);
-    dropzoneInst.handleFiles([file]);
+    // FIXME: this is not entirely correct because `file` does not satisfy DropzoneFile (we have abused the Dropzone for long time)
+    dropzoneInst.addFile(file as DropzoneFile);
   });
 }
 
 class TextareaEditor {
-  editor : HTMLTextAreaElement;
+  editor: HTMLTextAreaElement;
 
-  constructor(editor) {
+  constructor(editor: HTMLTextAreaElement) {
     this.editor = editor;
   }
 
-  insertPlaceholder(value) {
+  insertPlaceholder(value: string) {
     textareaInsertText(this.editor, value);
   }
 
-  replacePlaceholder(oldVal, newVal) {
+  replacePlaceholder(oldVal: string, newVal: string) {
     const editor = this.editor;
     const startPos = editor.selectionStart;
     const endPos = editor.selectionEnd;
@@ -65,11 +68,11 @@ class TextareaEditor {
 class CodeMirrorEditor {
   editor: CodeMirror.EditorFromTextArea;
 
-  constructor(editor) {
+  constructor(editor: CodeMirror.EditorFromTextArea) {
     this.editor = editor;
   }
 
-  insertPlaceholder(value) {
+  insertPlaceholder(value: string) {
     const editor = this.editor;
     const startPoint = editor.getCursor('start');
     const endPoint = editor.getCursor('end');
@@ -80,7 +83,7 @@ class CodeMirrorEditor {
     triggerEditorContentChanged(editor.getTextArea());
   }
 
-  replacePlaceholder(oldVal, newVal) {
+  replacePlaceholder(oldVal: string, newVal: string) {
     const editor = this.editor;
     const endPoint = editor.getCursor('end');
     if (editor.getSelection() === oldVal) {
@@ -96,7 +99,7 @@ class CodeMirrorEditor {
   }
 }
 
-async function handleUploadFiles(editor, dropzoneEl, files, e) {
+async function handleUploadFiles(editor: CodeMirrorEditor | TextareaEditor, dropzoneEl: HTMLElement, files: Array<File> | FileList, e: Event) {
   e.preventDefault();
   for (const file of files) {
     const name = file.name.slice(0, file.name.lastIndexOf('.'));
@@ -109,13 +112,13 @@ async function handleUploadFiles(editor, dropzoneEl, files, e) {
   }
 }
 
-export function removeAttachmentLinksFromMarkdown(text, fileUuid) {
+export function removeAttachmentLinksFromMarkdown(text: string, fileUuid: string) {
   text = text.replace(new RegExp(`!?\\[([^\\]]+)\\]\\(/?attachments/${fileUuid}\\)`, 'g'), '');
   text = text.replace(new RegExp(`<img[^>]+src="/?attachments/${fileUuid}"[^>]*>`, 'g'), '');
   return text;
 }
 
-function handleClipboardText(textarea, e, {text, isShiftDown}) {
+function handleClipboardText(textarea: HTMLTextAreaElement, e: ClipboardEvent, text: string, isShiftDown: boolean) {
   // pasting with "shift" means "paste as original content" in most applications
   if (isShiftDown) return; // let the browser handle it
 
@@ -131,7 +134,7 @@ function handleClipboardText(textarea, e, {text, isShiftDown}) {
 }
 
 // extract text and images from "paste" event
-function getPastedContent(e) {
+function getPastedContent(e: ClipboardEvent) {
   const images = [];
   for (const item of e.clipboardData?.items ?? []) {
     if (item.type?.startsWith('image/')) {
@@ -142,8 +145,8 @@ function getPastedContent(e) {
   return {text, images};
 }
 
-export function initEasyMDEPaste(easyMDE, dropzoneEl) {
-  const editor = new CodeMirrorEditor(easyMDE.codemirror);
+export function initEasyMDEPaste(easyMDE: EasyMDE, dropzoneEl: HTMLElement) {
+  const editor = new CodeMirrorEditor(easyMDE.codemirror as any);
   easyMDE.codemirror.on('paste', (_, e) => {
     const {images} = getPastedContent(e);
     if (!images.length) return;
@@ -160,28 +163,28 @@ export function initEasyMDEPaste(easyMDE, dropzoneEl) {
   });
 }
 
-export function initTextareaEvents(textarea, dropzoneEl) {
+export function initTextareaEvents(textarea: HTMLTextAreaElement, dropzoneEl: HTMLElement) {
   let isShiftDown = false;
-  textarea.addEventListener('keydown', (e) => {
+  textarea.addEventListener('keydown', (e: KeyboardEvent) => {
     if (e.shiftKey) isShiftDown = true;
   });
-  textarea.addEventListener('keyup', (e) => {
+  textarea.addEventListener('keyup', (e: KeyboardEvent) => {
     if (!e.shiftKey) isShiftDown = false;
   });
-  textarea.addEventListener('paste', (e) => {
+  textarea.addEventListener('paste', (e: ClipboardEvent) => {
     const {images, text} = getPastedContent(e);
     if (images.length && dropzoneEl) {
       handleUploadFiles(new TextareaEditor(textarea), dropzoneEl, images, e);
     } else if (text) {
-      handleClipboardText(textarea, e, {text, isShiftDown});
+      handleClipboardText(textarea, e, text, isShiftDown);
     }
   });
-  textarea.addEventListener('drop', (e) => {
+  textarea.addEventListener('drop', (e: DragEvent) => {
     if (!e.dataTransfer.files.length) return;
     if (!dropzoneEl) return;
     handleUploadFiles(new TextareaEditor(textarea), dropzoneEl, e.dataTransfer.files, e);
   });
-  dropzoneEl?.dropzone.on(DropzoneCustomEventRemovedFile, ({fileUuid}) => {
+  dropzoneEl?.dropzone.on(DropzoneCustomEventRemovedFile, ({fileUuid}: {fileUuid: string}) => {
     const newText = removeAttachmentLinksFromMarkdown(textarea.value, fileUuid);
     if (textarea.value !== newText) textarea.value = newText;
   });
diff --git a/web_src/js/features/comp/QuickSubmit.ts b/web_src/js/features/comp/QuickSubmit.ts
index 385acb319f..0a41f69132 100644
--- a/web_src/js/features/comp/QuickSubmit.ts
+++ b/web_src/js/features/comp/QuickSubmit.ts
@@ -1,6 +1,6 @@
 import {querySingleVisibleElem} from '../../utils/dom.ts';
 
-export function handleGlobalEnterQuickSubmit(target) {
+export function handleGlobalEnterQuickSubmit(target: HTMLElement) {
   let form = target.closest('form');
   if (form) {
     if (!form.checkValidity()) {
diff --git a/web_src/js/features/comp/SearchUserBox.ts b/web_src/js/features/comp/SearchUserBox.ts
index 2e3b3f83be..9fedb3ed24 100644
--- a/web_src/js/features/comp/SearchUserBox.ts
+++ b/web_src/js/features/comp/SearchUserBox.ts
@@ -14,7 +14,7 @@ export function initCompSearchUserBox() {
     minCharacters: 2,
     apiSettings: {
       url: `${appSubUrl}/user/search_candidates?q={query}`,
-      onResponse(response) {
+      onResponse(response: any) {
         const resultItems = [];
         const searchQuery = searchUserBox.querySelector('input').value;
         const searchQueryUppercase = searchQuery.toUpperCase();
diff --git a/web_src/js/features/comp/TextExpander.ts b/web_src/js/features/comp/TextExpander.ts
index e0c4abed75..dc08d1739d 100644
--- a/web_src/js/features/comp/TextExpander.ts
+++ b/web_src/js/features/comp/TextExpander.ts
@@ -1,14 +1,20 @@
 import {matchEmoji, matchMention, matchIssue} from '../../utils/match.ts';
 import {emojiString} from '../emoji.ts';
 import {svg} from '../../svg.ts';
-import {parseIssueHref, parseIssueNewHref} from '../../utils.ts';
+import {parseIssueHref, parseRepoOwnerPathInfo} from '../../utils.ts';
 import {createElementFromAttrs, createElementFromHTML} from '../../utils/dom.ts';
 import {getIssueColor, getIssueIcon} from '../issue.ts';
 import {debounce} from 'perfect-debounce';
+import type TextExpanderElement from '@github/text-expander-element';
 
 const debouncedSuggestIssues = debounce((key: string, text: string) => new Promise<{matched:boolean; fragment?: HTMLElement}>(async (resolve) => {
-  let issuePathInfo = parseIssueHref(window.location.href);
-  if (!issuePathInfo.ownerName) issuePathInfo = parseIssueNewHref(window.location.href);
+  const issuePathInfo = parseIssueHref(window.location.href);
+  if (!issuePathInfo.ownerName) {
+    const repoOwnerPathInfo = parseRepoOwnerPathInfo(window.location.pathname);
+    issuePathInfo.ownerName = repoOwnerPathInfo.ownerName;
+    issuePathInfo.repoName = repoOwnerPathInfo.repoName;
+    // then no issuePathInfo.indexString here, it is only used to exclude the current issue when "matchIssue"
+  }
   if (!issuePathInfo.ownerName) return resolve({matched: false});
 
   const matches = await matchIssue(issuePathInfo.ownerName, issuePathInfo.repoName, issuePathInfo.indexString, text);
@@ -27,8 +33,8 @@ const debouncedSuggestIssues = debounce((key: string, text: string) => new Promi
   resolve({matched: true, fragment: ul});
 }), 100);
 
-export function initTextExpander(expander) {
-  expander?.addEventListener('text-expander-change', ({detail: {key, provide, text}}) => {
+export function initTextExpander(expander: TextExpanderElement) {
+  expander?.addEventListener('text-expander-change', ({detail: {key, provide, text}}: Record<string, any>) => {
     if (key === ':') {
       const matches = matchEmoji(text);
       if (!matches.length) return provide({matched: false});
@@ -79,7 +85,7 @@ export function initTextExpander(expander) {
       provide(debouncedSuggestIssues(key, text));
     }
   });
-  expander?.addEventListener('text-expander-value', ({detail}) => {
+  expander?.addEventListener('text-expander-value', ({detail}: Record<string, any>) => {
     if (detail?.item) {
       // add a space after @mentions and #issue as it's likely the user wants one
       const suffix = ['@', '#'].includes(detail.key) ? ' ' : '';
diff --git a/web_src/js/features/comp/WebHookEditor.ts b/web_src/js/features/comp/WebHookEditor.ts
index 203396af80..794b3c99ca 100644
--- a/web_src/js/features/comp/WebHookEditor.ts
+++ b/web_src/js/features/comp/WebHookEditor.ts
@@ -6,7 +6,7 @@ export function initCompWebHookEditor() {
     return;
   }
 
-  for (const input of document.querySelectorAll('.events.checkbox input')) {
+  for (const input of document.querySelectorAll<HTMLInputElement>('.events.checkbox input')) {
     input.addEventListener('change', function () {
       if (this.checked) {
         showElem('.events.fields');
@@ -14,7 +14,7 @@ export function initCompWebHookEditor() {
     });
   }
 
-  for (const input of document.querySelectorAll('.non-events.checkbox input')) {
+  for (const input of document.querySelectorAll<HTMLInputElement>('.non-events.checkbox input')) {
     input.addEventListener('change', function () {
       if (this.checked) {
         hideElem('.events.fields');
@@ -34,7 +34,7 @@ export function initCompWebHookEditor() {
   }
 
   // Test delivery
-  document.querySelector('#test-delivery')?.addEventListener('click', async function () {
+  document.querySelector<HTMLButtonElement>('#test-delivery')?.addEventListener('click', async function () {
     this.classList.add('is-loading', 'disabled');
     await POST(this.getAttribute('data-link'));
     setTimeout(() => {
diff --git a/web_src/js/features/contextpopup.ts b/web_src/js/features/contextpopup.ts
index 33eead8431..7477331dbe 100644
--- a/web_src/js/features/contextpopup.ts
+++ b/web_src/js/features/contextpopup.ts
@@ -4,11 +4,11 @@ import {parseIssueHref} from '../utils.ts';
 import {createTippy} from '../modules/tippy.ts';
 
 export function initContextPopups() {
-  const refIssues = document.querySelectorAll('.ref-issue');
+  const refIssues = document.querySelectorAll<HTMLElement>('.ref-issue');
   attachRefIssueContextPopup(refIssues);
 }
 
-export function attachRefIssueContextPopup(refIssues) {
+export function attachRefIssueContextPopup(refIssues: NodeListOf<HTMLElement>) {
   for (const refIssue of refIssues) {
     if (refIssue.classList.contains('ref-external-issue')) continue;
 
diff --git a/web_src/js/features/copycontent.ts b/web_src/js/features/copycontent.ts
index af867463b2..4bc9281a35 100644
--- a/web_src/js/features/copycontent.ts
+++ b/web_src/js/features/copycontent.ts
@@ -46,7 +46,7 @@ export function initCopyContent() {
       showTemporaryTooltip(btn, i18n.copy_success);
     } else {
       if (isRasterImage) {
-        const success = await clippie(await convertImage(content, 'image/png'));
+        const success = await clippie(await convertImage(content as Blob, 'image/png'));
         showTemporaryTooltip(btn, success ? i18n.copy_success : i18n.copy_error);
       } else {
         showTemporaryTooltip(btn, i18n.copy_error);
diff --git a/web_src/js/features/dashboard.ts b/web_src/js/features/dashboard.ts
new file mode 100644
index 0000000000..71a0df3a64
--- /dev/null
+++ b/web_src/js/features/dashboard.ts
@@ -0,0 +1,9 @@
+import {createApp} from 'vue';
+import DashboardRepoList from '../components/DashboardRepoList.vue';
+
+export function initDashboardRepoList() {
+  const el = document.querySelector('#dashboard-repo-list');
+  if (el) {
+    createApp(DashboardRepoList).mount(el);
+  }
+}
diff --git a/web_src/js/features/dropzone.ts b/web_src/js/features/dropzone.ts
index 666c645230..b2ba7651c4 100644
--- a/web_src/js/features/dropzone.ts
+++ b/web_src/js/features/dropzone.ts
@@ -6,16 +6,18 @@ import {GET, POST} from '../modules/fetch.ts';
 import {showErrorToast} from '../modules/toast.ts';
 import {createElementFromHTML, createElementFromAttrs} from '../utils/dom.ts';
 import {isImageFile, isVideoFile} from '../utils.ts';
-import type {DropzoneFile} from 'dropzone/index.js';
+import type {DropzoneFile, DropzoneOptions} from 'dropzone/index.js';
 
 const {csrfToken, i18n} = window.config;
 
+type CustomDropzoneFile = DropzoneFile & {uuid: string};
+
 // dropzone has its owner event dispatcher (emitter)
 export const DropzoneCustomEventReloadFiles = 'dropzone-custom-reload-files';
 export const DropzoneCustomEventRemovedFile = 'dropzone-custom-removed-file';
 export const DropzoneCustomEventUploadDone = 'dropzone-custom-upload-done';
 
-async function createDropzone(el, opts) {
+async function createDropzone(el: HTMLElement, opts: DropzoneOptions) {
   const [{default: Dropzone}] = await Promise.all([
     import(/* webpackChunkName: "dropzone" */'dropzone'),
     import(/* webpackChunkName: "dropzone" */'dropzone/dist/dropzone.css'),
@@ -23,7 +25,7 @@ async function createDropzone(el, opts) {
   return new Dropzone(el, opts);
 }
 
-export function generateMarkdownLinkForAttachment(file, {width, dppx}: {width?: number, dppx?: number} = {}) {
+export function generateMarkdownLinkForAttachment(file: Partial<CustomDropzoneFile>, {width, dppx}: {width?: number, dppx?: number} = {}) {
   let fileMarkdown = `[${file.name}](/attachments/${file.uuid})`;
   if (isImageFile(file)) {
     fileMarkdown = `!${fileMarkdown}`;
@@ -43,7 +45,7 @@ export function generateMarkdownLinkForAttachment(file, {width, dppx}: {width?:
   return fileMarkdown;
 }
 
-function addCopyLink(file) {
+function addCopyLink(file: Partial<CustomDropzoneFile>) {
   // Create a "Copy Link" element, to conveniently copy the image or file link as Markdown to the clipboard
   // The "<a>" element has a hardcoded cursor: pointer because the default is overridden by .dropzone
   const copyLinkEl = createElementFromHTML(`
@@ -58,6 +60,8 @@ function addCopyLink(file) {
   file.previewTemplate.append(copyLinkEl);
 }
 
+type FileUuidDict = Record<string, {submitted: boolean}>;
+
 /**
  * @param {HTMLElement} dropzoneEl
  */
@@ -67,7 +71,7 @@ export async function initDropzone(dropzoneEl: HTMLElement) {
   const attachmentBaseLinkUrl = dropzoneEl.getAttribute('data-link-url');
 
   let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event
-  let fileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone
+  let fileUuidDict: FileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone
   const opts: Record<string, any> = {
     url: dropzoneEl.getAttribute('data-upload-url'),
     headers: {'X-Csrf-Token': csrfToken},
@@ -89,7 +93,7 @@ export async function initDropzone(dropzoneEl: HTMLElement) {
   // "http://localhost:3000/owner/repo/issues/[object%20Event]"
   // the reason is that the preview "callback(dataURL)" is assign to "img.onerror" then "thumbnail" uses the error object as the dataURL and generates '<img src="[object Event]">'
   const dzInst = await createDropzone(dropzoneEl, opts);
-  dzInst.on('success', (file: DropzoneFile & {uuid: string}, resp: any) => {
+  dzInst.on('success', (file: CustomDropzoneFile, resp: any) => {
     file.uuid = resp.uuid;
     fileUuidDict[file.uuid] = {submitted: false};
     const input = createElementFromAttrs('input', {name: 'files', type: 'hidden', id: `dropzone-file-${resp.uuid}`, value: resp.uuid});
@@ -98,7 +102,7 @@ export async function initDropzone(dropzoneEl: HTMLElement) {
     dzInst.emit(DropzoneCustomEventUploadDone, {file});
   });
 
-  dzInst.on('removedfile', async (file: DropzoneFile & {uuid: string}) => {
+  dzInst.on('removedfile', async (file: CustomDropzoneFile) => {
     if (disableRemovedfileEvent) return;
 
     dzInst.emit(DropzoneCustomEventRemovedFile, {fileUuid: file.uuid});
diff --git a/web_src/js/features/emoji.ts b/web_src/js/features/emoji.ts
index 933aa951c5..135620e51e 100644
--- a/web_src/js/features/emoji.ts
+++ b/web_src/js/features/emoji.ts
@@ -15,13 +15,13 @@ export const emojiKeys = Object.keys(tempMap).sort((a, b) => {
   return a.localeCompare(b);
 });
 
-const emojiMap = {};
+const emojiMap: Record<string, string> = {};
 for (const key of emojiKeys) {
   emojiMap[key] = tempMap[key];
 }
 
 // retrieve HTML for given emoji name
-export function emojiHTML(name) {
+export function emojiHTML(name: string) {
   let inner;
   if (Object.hasOwn(customEmojis, name)) {
     inner = `<img alt=":${name}:" src="${assetUrlPrefix}/img/emoji/${name}.png">`;
@@ -33,6 +33,6 @@ export function emojiHTML(name) {
 }
 
 // retrieve string for given emoji name
-export function emojiString(name) {
+export function emojiString(name: string) {
   return emojiMap[name] || `:${name}:`;
 }
diff --git a/web_src/js/features/file-fold.ts b/web_src/js/features/file-fold.ts
index 6fe068341a..19950d9b9f 100644
--- a/web_src/js/features/file-fold.ts
+++ b/web_src/js/features/file-fold.ts
@@ -5,15 +5,15 @@ import {svg} from '../svg.ts';
 // The fold arrow is the icon displayed on the upper left of the file box, especially intended for components having the 'fold-file' class.
 // The file content box is the box that should be hidden or shown, especially intended for components having the 'file-content' class.
 //
-export function setFileFolding(fileContentBox, foldArrow, newFold) {
+export function setFileFolding(fileContentBox: HTMLElement, foldArrow: HTMLElement, newFold: boolean) {
   foldArrow.innerHTML = svg(`octicon-chevron-${newFold ? 'right' : 'down'}`, 18);
-  fileContentBox.setAttribute('data-folded', newFold);
+  fileContentBox.setAttribute('data-folded', String(newFold));
   if (newFold && fileContentBox.getBoundingClientRect().top < 0) {
     fileContentBox.scrollIntoView();
   }
 }
 
 // Like `setFileFolding`, except that it automatically inverts the current file folding state.
-export function invertFileFolding(fileContentBox, foldArrow) {
+export function invertFileFolding(fileContentBox:HTMLElement, foldArrow: HTMLElement) {
   setFileFolding(fileContentBox, foldArrow, fileContentBox.getAttribute('data-folded') !== 'true');
 }
diff --git a/web_src/js/features/heatmap.ts b/web_src/js/features/heatmap.ts
index 53eebc93e5..7cec82108b 100644
--- a/web_src/js/features/heatmap.ts
+++ b/web_src/js/features/heatmap.ts
@@ -7,7 +7,7 @@ export function initHeatmap() {
   if (!el) return;
 
   try {
-    const heatmap = {};
+    const heatmap: Record<string, number> = {};
     for (const {contributions, timestamp} of JSON.parse(el.getAttribute('data-heatmap-data'))) {
       // Convert to user timezone and sum contributions by date
       const dateStr = new Date(timestamp * 1000).toDateString();
diff --git a/web_src/js/features/imagediff.ts b/web_src/js/features/imagediff.ts
index cd61888f83..e62734293a 100644
--- a/web_src/js/features/imagediff.ts
+++ b/web_src/js/features/imagediff.ts
@@ -3,7 +3,7 @@ import {hideElem, loadElem, queryElemChildren, queryElems} from '../utils/dom.ts
 import {parseDom} from '../utils.ts';
 import {fomanticQuery} from '../modules/fomantic/base.ts';
 
-function getDefaultSvgBoundsIfUndefined(text, src) {
+function getDefaultSvgBoundsIfUndefined(text: string, src: string) {
   const defaultSize = 300;
   const maxSize = 99999;
 
@@ -38,7 +38,7 @@ function getDefaultSvgBoundsIfUndefined(text, src) {
   return null;
 }
 
-function createContext(imageAfter, imageBefore) {
+function createContext(imageAfter: HTMLImageElement, imageBefore: HTMLImageElement) {
   const sizeAfter = {
     width: imageAfter?.width || 0,
     height: imageAfter?.height || 0,
@@ -123,7 +123,7 @@ class ImageDiff {
     queryElemChildren(containerEl, '.image-diff-tabs', (el) => el.classList.remove('is-loading'));
   }
 
-  initSideBySide(sizes) {
+  initSideBySide(sizes: Record<string, any>) {
     let factor = 1;
     if (sizes.maxSize.width > (this.diffContainerWidth - 24) / 2) {
       factor = (this.diffContainerWidth - 24) / 2 / sizes.maxSize.width;
@@ -176,7 +176,7 @@ class ImageDiff {
     }
   }
 
-  initSwipe(sizes) {
+  initSwipe(sizes: Record<string, any>) {
     let factor = 1;
     if (sizes.maxSize.width > this.diffContainerWidth - 12) {
       factor = (this.diffContainerWidth - 12) / sizes.maxSize.width;
@@ -215,14 +215,14 @@ class ImageDiff {
 
     this.containerEl.querySelector('.swipe-bar').addEventListener('mousedown', (e) => {
       e.preventDefault();
-      this.initSwipeEventListeners(e.currentTarget);
+      this.initSwipeEventListeners(e.currentTarget as HTMLElement);
     });
   }
 
-  initSwipeEventListeners(swipeBar) {
-    const swipeFrame = swipeBar.parentNode;
+  initSwipeEventListeners(swipeBar: HTMLElement) {
+    const swipeFrame = swipeBar.parentNode as HTMLElement;
     const width = swipeFrame.clientWidth;
-    const onSwipeMouseMove = (e) => {
+    const onSwipeMouseMove = (e: MouseEvent) => {
       e.preventDefault();
       const rect = swipeFrame.getBoundingClientRect();
       const value = Math.max(0, Math.min(e.clientX - rect.left, width));
@@ -237,7 +237,7 @@ class ImageDiff {
     document.addEventListener('mouseup', removeEventListeners);
   }
 
-  initOverlay(sizes) {
+  initOverlay(sizes: Record<string, any>) {
     let factor = 1;
     if (sizes.maxSize.width > this.diffContainerWidth - 12) {
       factor = (this.diffContainerWidth - 12) / sizes.maxSize.width;
diff --git a/web_src/js/features/install.ts b/web_src/js/features/install.ts
index 725dcafab0..34df4757f9 100644
--- a/web_src/js/features/install.ts
+++ b/web_src/js/features/install.ts
@@ -12,11 +12,12 @@ export function initInstall() {
     initPreInstall();
   }
 }
+
 function initPreInstall() {
   const defaultDbUser = 'gitea';
   const defaultDbName = 'gitea';
 
-  const defaultDbHosts = {
+  const defaultDbHosts: Record<string, string> = {
     mysql: '127.0.0.1:3306',
     postgres: '127.0.0.1:5432',
     mssql: '127.0.0.1:1433',
@@ -27,7 +28,7 @@ function initPreInstall() {
   const dbName = document.querySelector<HTMLInputElement>('#db_name');
 
   // Database type change detection.
-  document.querySelector('#db_type').addEventListener('change', function () {
+  document.querySelector<HTMLInputElement>('#db_type').addEventListener('change', function () {
     const dbType = this.value;
     hideElem('div[data-db-setting-for]');
     showElem(`div[data-db-setting-for=${dbType}]`);
@@ -59,26 +60,26 @@ function initPreInstall() {
   }
 
   // TODO: better handling of exclusive relations.
-  document.querySelector('#offline-mode input').addEventListener('change', function () {
+  document.querySelector<HTMLInputElement>('#offline-mode input').addEventListener('change', function () {
     if (this.checked) {
       document.querySelector<HTMLInputElement>('#disable-gravatar input').checked = true;
       document.querySelector<HTMLInputElement>('#federated-avatar-lookup input').checked = false;
     }
   });
-  document.querySelector('#disable-gravatar input').addEventListener('change', function () {
+  document.querySelector<HTMLInputElement>('#disable-gravatar input').addEventListener('change', function () {
     if (this.checked) {
       document.querySelector<HTMLInputElement>('#federated-avatar-lookup input').checked = false;
     } else {
       document.querySelector<HTMLInputElement>('#offline-mode input').checked = false;
     }
   });
-  document.querySelector('#federated-avatar-lookup input').addEventListener('change', function () {
+  document.querySelector<HTMLInputElement>('#federated-avatar-lookup input').addEventListener('change', function () {
     if (this.checked) {
       document.querySelector<HTMLInputElement>('#disable-gravatar input').checked = false;
       document.querySelector<HTMLInputElement>('#offline-mode input').checked = false;
     }
   });
-  document.querySelector('#enable-openid-signin input').addEventListener('change', function () {
+  document.querySelector<HTMLInputElement>('#enable-openid-signin input').addEventListener('change', function () {
     if (this.checked) {
       if (!document.querySelector<HTMLInputElement>('#disable-registration input').checked) {
         document.querySelector<HTMLInputElement>('#enable-openid-signup input').checked = true;
@@ -87,7 +88,7 @@ function initPreInstall() {
       document.querySelector<HTMLInputElement>('#enable-openid-signup input').checked = false;
     }
   });
-  document.querySelector('#disable-registration input').addEventListener('change', function () {
+  document.querySelector<HTMLInputElement>('#disable-registration input').addEventListener('change', function () {
     if (this.checked) {
       document.querySelector<HTMLInputElement>('#enable-captcha input').checked = false;
       document.querySelector<HTMLInputElement>('#enable-openid-signup input').checked = false;
@@ -95,7 +96,7 @@ function initPreInstall() {
       document.querySelector<HTMLInputElement>('#enable-openid-signup input').checked = true;
     }
   });
-  document.querySelector('#enable-captcha input').addEventListener('change', function () {
+  document.querySelector<HTMLInputElement>('#enable-captcha input').addEventListener('change', function () {
     if (this.checked) {
       document.querySelector<HTMLInputElement>('#disable-registration input').checked = false;
     }
diff --git a/web_src/js/features/issue.ts b/web_src/js/features/issue.ts
index a56015a2a2..911cf713d9 100644
--- a/web_src/js/features/issue.ts
+++ b/web_src/js/features/issue.ts
@@ -1,17 +1,21 @@
 import type {Issue} from '../types.ts';
 
+// the getIssueIcon/getIssueColor logic should be kept the same as "templates/shared/issueicon.tmpl"
+
 export function getIssueIcon(issue: Issue) {
   if (issue.pull_request) {
     if (issue.state === 'open') {
-      if (issue.pull_request.draft === true) {
+      if (issue.pull_request.draft) {
         return 'octicon-git-pull-request-draft'; // WIP PR
       }
       return 'octicon-git-pull-request'; // Open PR
-    } else if (issue.pull_request.merged === true) {
+    } else if (issue.pull_request.merged) {
       return 'octicon-git-merge'; // Merged PR
     }
-    return 'octicon-git-pull-request'; // Closed PR
-  } else if (issue.state === 'open') {
+    return 'octicon-git-pull-request-closed'; // Closed PR
+  }
+
+  if (issue.state === 'open') {
     return 'octicon-issue-opened'; // Open Issue
   }
   return 'octicon-issue-closed'; // Closed Issue
@@ -19,12 +23,17 @@ export function getIssueIcon(issue: Issue) {
 
 export function getIssueColor(issue: Issue) {
   if (issue.pull_request) {
-    if (issue.pull_request.draft === true) {
-      return 'grey'; // WIP PR
-    } else if (issue.pull_request.merged === true) {
+    if (issue.state === 'open') {
+      if (issue.pull_request.draft) {
+        return 'grey'; // WIP PR
+      }
+      return 'green'; // Open PR
+    } else if (issue.pull_request.merged) {
       return 'purple'; // Merged PR
     }
+    return 'red'; // Closed PR
   }
+
   if (issue.state === 'open') {
     return 'green'; // Open Issue
   }
diff --git a/web_src/js/features/org-team.ts b/web_src/js/features/org-team.ts
index e160f07bf2..d07818b0ac 100644
--- a/web_src/js/features/org-team.ts
+++ b/web_src/js/features/org-team.ts
@@ -21,7 +21,7 @@ function initOrgTeamSearchRepoBox() {
     minCharacters: 2,
     apiSettings: {
       url: `${appSubUrl}/repo/search?q={query}&uid=${$searchRepoBox.data('uid')}`,
-      onResponse(response) {
+      onResponse(response: any) {
         const items = [];
         for (const item of response.data) {
           items.push({
diff --git a/web_src/js/features/pull-view-file.ts b/web_src/js/features/pull-view-file.ts
index 36fe4bc4df..16ccf00084 100644
--- a/web_src/js/features/pull-view-file.ts
+++ b/web_src/js/features/pull-view-file.ts
@@ -38,7 +38,7 @@ export function initViewedCheckboxListenerFor() {
 
     // The checkbox consists of a div containing the real checkbox with its label and the CSRF token,
     // hence the actual checkbox first has to be found
-    const checkbox = form.querySelector('input[type=checkbox]');
+    const checkbox = form.querySelector<HTMLInputElement>('input[type=checkbox]');
     checkbox.addEventListener('input', function() {
       // Mark the file as viewed visually - will especially change the background
       if (this.checked) {
@@ -59,13 +59,13 @@ export function initViewedCheckboxListenerFor() {
       const fileName = checkbox.getAttribute('name');
 
       // check if the file is in our difftreestore and if we find it -> change the IsViewed status
-      const fileInPageData = diffTreeStore().files.find((x) => x.Name === fileName);
+      const fileInPageData = diffTreeStore().files.find((x: Record<string, any>) => x.Name === fileName);
       if (fileInPageData) {
         fileInPageData.IsViewed = this.checked;
       }
 
       // Unfortunately, actual forms cause too many problems, hence another approach is needed
-      const files = {};
+      const files: Record<string, boolean> = {};
       files[fileName] = this.checked;
       const data: Record<string, any> = {files};
       const headCommitSHA = form.getAttribute('data-headcommit');
@@ -82,13 +82,13 @@ export function initViewedCheckboxListenerFor() {
 export function initExpandAndCollapseFilesButton() {
   // expand btn
   document.querySelector(expandFilesBtnSelector)?.addEventListener('click', () => {
-    for (const box of document.querySelectorAll('.file-content[data-folded="true"]')) {
+    for (const box of document.querySelectorAll<HTMLElement>('.file-content[data-folded="true"]')) {
       setFileFolding(box, box.querySelector('.fold-file'), false);
     }
   });
   // collapse btn, need to exclude the div of “show more”
   document.querySelector(collapseFilesBtnSelector)?.addEventListener('click', () => {
-    for (const box of document.querySelectorAll('.file-content:not([data-folded="true"])')) {
+    for (const box of document.querySelectorAll<HTMLElement>('.file-content:not([data-folded="true"])')) {
       if (box.getAttribute('id') === 'diff-incomplete') continue;
       setFileFolding(box, box.querySelector('.fold-file'), true);
     }
diff --git a/web_src/js/features/repo-actions.ts b/web_src/js/features/repo-actions.ts
new file mode 100644
index 0000000000..cbd0429c04
--- /dev/null
+++ b/web_src/js/features/repo-actions.ts
@@ -0,0 +1,47 @@
+import {createApp} from 'vue';
+import RepoActionView from '../components/RepoActionView.vue';
+
+export function initRepositoryActionView() {
+  const el = document.querySelector('#repo-action-view');
+  if (!el) return;
+
+  // TODO: the parent element's full height doesn't work well now,
+  // but we can not pollute the global style at the moment, only fix the height problem for pages with this component
+  const parentFullHeight = document.querySelector<HTMLElement>('body > div.full.height');
+  if (parentFullHeight) parentFullHeight.style.paddingBottom = '0';
+
+  const view = createApp(RepoActionView, {
+    runIndex: el.getAttribute('data-run-index'),
+    jobIndex: el.getAttribute('data-job-index'),
+    actionsURL: el.getAttribute('data-actions-url'),
+    locale: {
+      approve: el.getAttribute('data-locale-approve'),
+      cancel: el.getAttribute('data-locale-cancel'),
+      rerun: el.getAttribute('data-locale-rerun'),
+      rerun_all: el.getAttribute('data-locale-rerun-all'),
+      scheduled: el.getAttribute('data-locale-runs-scheduled'),
+      commit: el.getAttribute('data-locale-runs-commit'),
+      pushedBy: el.getAttribute('data-locale-runs-pushed-by'),
+      artifactsTitle: el.getAttribute('data-locale-artifacts-title'),
+      areYouSure: el.getAttribute('data-locale-are-you-sure'),
+      confirmDeleteArtifact: el.getAttribute('data-locale-confirm-delete-artifact'),
+      showTimeStamps: el.getAttribute('data-locale-show-timestamps'),
+      showLogSeconds: el.getAttribute('data-locale-show-log-seconds'),
+      showFullScreen: el.getAttribute('data-locale-show-full-screen'),
+      downloadLogs: el.getAttribute('data-locale-download-logs'),
+      status: {
+        unknown: el.getAttribute('data-locale-status-unknown'),
+        waiting: el.getAttribute('data-locale-status-waiting'),
+        running: el.getAttribute('data-locale-status-running'),
+        success: el.getAttribute('data-locale-status-success'),
+        failure: el.getAttribute('data-locale-status-failure'),
+        cancelled: el.getAttribute('data-locale-status-cancelled'),
+        skipped: el.getAttribute('data-locale-status-skipped'),
+        blocked: el.getAttribute('data-locale-status-blocked'),
+      },
+      logsAlwaysAutoScroll: el.getAttribute('data-locale-logs-always-auto-scroll'),
+      logsAlwaysExpandRunning: el.getAttribute('data-locale-logs-always-expand-running'),
+    },
+  });
+  view.mount(el);
+}
diff --git a/web_src/js/features/repo-commit.ts b/web_src/js/features/repo-commit.ts
index 56493443d9..8994a57f4a 100644
--- a/web_src/js/features/repo-commit.ts
+++ b/web_src/js/features/repo-commit.ts
@@ -2,7 +2,7 @@ import {createTippy} from '../modules/tippy.ts';
 import {toggleElem} from '../utils/dom.ts';
 
 export function initRepoEllipsisButton() {
-  for (const button of document.querySelectorAll('.js-toggle-commit-body')) {
+  for (const button of document.querySelectorAll<HTMLButtonElement>('.js-toggle-commit-body')) {
     button.addEventListener('click', function (e) {
       e.preventDefault();
       const expanded = this.getAttribute('aria-expanded') === 'true';
diff --git a/web_src/js/features/repo-common.ts b/web_src/js/features/repo-common.ts
index 90860720e4..fb76d8ed36 100644
--- a/web_src/js/features/repo-common.ts
+++ b/web_src/js/features/repo-common.ts
@@ -1,4 +1,4 @@
-import {queryElems} from '../utils/dom.ts';
+import {queryElems, type DOMEvent} from '../utils/dom.ts';
 import {POST} from '../modules/fetch.ts';
 import {showErrorToast} from '../modules/toast.ts';
 import {sleep} from '../utils.ts';
@@ -7,10 +7,10 @@ import {createApp} from 'vue';
 import {toOriginUrl} from '../utils/url.ts';
 import {createTippy} from '../modules/tippy.ts';
 
-async function onDownloadArchive(e) {
+async function onDownloadArchive(e: DOMEvent<MouseEvent>) {
   e.preventDefault();
   // there are many places using the "archive-link", eg: the dropdown on the repo code page, the release list
-  const el = e.target.closest('a.archive-link[href]');
+  const el = e.target.closest<HTMLAnchorElement>('a.archive-link[href]');
   const targetLoading = el.closest('.ui.dropdown') ?? el;
   targetLoading.classList.add('is-loading', 'loading-icon-2px');
   try {
@@ -107,7 +107,7 @@ export function initRepoCloneButtons() {
   queryElems(document, '.clone-buttons-combo', initCloneSchemeUrlSelection);
 }
 
-export async function updateIssuesMeta(url, action, issue_ids, id) {
+export async function updateIssuesMeta(url: string, action: string, issue_ids: string, id: string) {
   try {
     const response = await POST(url, {data: new URLSearchParams({action, issue_ids, id})});
     if (!response.ok) {
diff --git a/web_src/js/features/repo-diff.ts b/web_src/js/features/repo-diff.ts
index 0cb2e566c0..0dad4da862 100644
--- a/web_src/js/features/repo-diff.ts
+++ b/web_src/js/features/repo-diff.ts
@@ -168,7 +168,7 @@ function onShowMoreFiles() {
   initDiffHeaderPopup();
 }
 
-export async function loadMoreFiles(url) {
+export async function loadMoreFiles(url: string) {
   const target = document.querySelector('a#diff-show-more-files');
   if (target?.classList.contains('disabled') || pageData.diffFileInfo.isLoadingNewData) {
     return;
diff --git a/web_src/js/features/repo-editor.ts b/web_src/js/features/repo-editor.ts
index d7097787d2..0f3fb7bbcf 100644
--- a/web_src/js/features/repo-editor.ts
+++ b/web_src/js/features/repo-editor.ts
@@ -168,7 +168,7 @@ export function initRepoEditor() {
       silent: true,
       dirtyClass: dirtyFileClass,
       fieldSelector: ':input:not(.commit-form-wrapper :input)',
-      change($form) {
+      change($form: any) {
         const dirty = $form[0]?.classList.contains(dirtyFileClass);
         commitButton.disabled = !dirty;
       },
diff --git a/web_src/js/features/repo-findfile.ts b/web_src/js/features/repo-findfile.ts
index 6500978bc8..59c827126f 100644
--- a/web_src/js/features/repo-findfile.ts
+++ b/web_src/js/features/repo-findfile.ts
@@ -4,13 +4,15 @@ import {pathEscapeSegments} from '../utils/url.ts';
 import {GET} from '../modules/fetch.ts';
 
 const threshold = 50;
-let files = [];
-let repoFindFileInput, repoFindFileTableBody, repoFindFileNoResult;
+let files: Array<string> = [];
+let repoFindFileInput: HTMLInputElement;
+let repoFindFileTableBody: HTMLElement;
+let repoFindFileNoResult: HTMLElement;
 
 // return the case-insensitive sub-match result as an array:  [unmatched, matched, unmatched, matched, ...]
 // res[even] is unmatched, res[odd] is matched, see unit tests for examples
 // argument subLower must be a lower-cased string.
-export function strSubMatch(full, subLower) {
+export function strSubMatch(full: string, subLower: string) {
   const res = [''];
   let i = 0, j = 0;
   const fullLower = full.toLowerCase();
@@ -38,7 +40,7 @@ export function strSubMatch(full, subLower) {
   return res;
 }
 
-export function calcMatchedWeight(matchResult) {
+export function calcMatchedWeight(matchResult: Array<any>) {
   let weight = 0;
   for (let i = 0; i < matchResult.length; i++) {
     if (i % 2 === 1) { // matches are on odd indices, see strSubMatch
@@ -49,7 +51,7 @@ export function calcMatchedWeight(matchResult) {
   return weight;
 }
 
-export function filterRepoFilesWeighted(files, filter) {
+export function filterRepoFilesWeighted(files: Array<string>, filter: string) {
   let filterResult = [];
   if (filter) {
     const filterLower = filter.toLowerCase();
@@ -71,7 +73,7 @@ export function filterRepoFilesWeighted(files, filter) {
   return filterResult;
 }
 
-function filterRepoFiles(filter) {
+function filterRepoFiles(filter: string) {
   const treeLink = repoFindFileInput.getAttribute('data-url-tree-link');
   repoFindFileTableBody.innerHTML = '';
 
diff --git a/web_src/js/features/repo-home.ts b/web_src/js/features/repo-home.ts
index 9c1e317486..04a1288626 100644
--- a/web_src/js/features/repo-home.ts
+++ b/web_src/js/features/repo-home.ts
@@ -89,10 +89,10 @@ export function initRepoTopicBar() {
       url: `${appSubUrl}/explore/topics/search?q={query}`,
       throttle: 500,
       cache: false,
-      onResponse(res) {
+      onResponse(this: any, res: any) {
         const formattedResponse = {
           success: false,
-          results: [],
+          results: [] as Array<Record<string, any>>,
         };
         const query = stripTags(this.urlData.query.trim());
         let found_query = false;
@@ -134,12 +134,12 @@ export function initRepoTopicBar() {
         return formattedResponse;
       },
     },
-    onLabelCreate(value) {
+    onLabelCreate(value: string) {
       value = value.toLowerCase().trim();
       this.attr('data-value', value).contents().first().replaceWith(value);
       return fomanticQuery(this);
     },
-    onAdd(addedValue, _addedText, $addedChoice) {
+    onAdd(addedValue: string, _addedText: any, $addedChoice: any) {
       addedValue = addedValue.toLowerCase().trim();
       $addedChoice[0].setAttribute('data-value', addedValue);
       $addedChoice[0].setAttribute('data-text', addedValue);
diff --git a/web_src/js/features/repo-issue-content.ts b/web_src/js/features/repo-issue-content.ts
index 2279c26beb..056b810be8 100644
--- a/web_src/js/features/repo-issue-content.ts
+++ b/web_src/js/features/repo-issue-content.ts
@@ -33,7 +33,7 @@ function showContentHistoryDetail(issueBaseUrl: string, commentId: string, histo
   $fomanticDropdownOptions.dropdown({
     showOnFocus: false,
     allowReselection: true,
-    async onChange(_value, _text, $item) {
+    async onChange(_value: string, _text: string, $item: any) {
       const optionItem = $item.data('option-item');
       if (optionItem === 'delete') {
         if (window.confirm(i18nTextDeleteFromHistoryConfirm)) {
@@ -115,7 +115,7 @@ function showContentHistoryMenu(issueBaseUrl: string, elCommentItem: Element, co
     onHide() {
       $fomanticDropdown.dropdown('change values', null);
     },
-    onChange(value, itemHtml, $item) {
+    onChange(value: string, itemHtml: string, $item: any) {
       if (value && !$item.find('[data-history-is-deleted=1]').length) {
         showContentHistoryDetail(issueBaseUrl, commentId, value, itemHtml);
       }
diff --git a/web_src/js/features/repo-issue-edit.ts b/web_src/js/features/repo-issue-edit.ts
index 38dfea4743..a3a13a156d 100644
--- a/web_src/js/features/repo-issue-edit.ts
+++ b/web_src/js/features/repo-issue-edit.ts
@@ -2,14 +2,14 @@ import {handleReply} from './repo-issue.ts';
 import {getComboMarkdownEditor, initComboMarkdownEditor, ComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
 import {POST} from '../modules/fetch.ts';
 import {showErrorToast} from '../modules/toast.ts';
-import {hideElem, querySingleVisibleElem, showElem} from '../utils/dom.ts';
+import {hideElem, querySingleVisibleElem, showElem, type DOMEvent} from '../utils/dom.ts';
 import {attachRefIssueContextPopup} from './contextpopup.ts';
 import {initCommentContent, initMarkupContent} from '../markup/content.ts';
 import {triggerUploadStateChanged} from './comp/EditorUpload.ts';
 import {convertHtmlToMarkdown} from '../markup/html2markdown.ts';
 import {applyAreYouSure, reinitializeAreYouSure} from '../vendor/jquery.are-you-sure.ts';
 
-async function tryOnEditContent(e) {
+async function tryOnEditContent(e: DOMEvent<MouseEvent>) {
   const clickTarget = e.target.closest('.edit-content');
   if (!clickTarget) return;
 
@@ -21,14 +21,14 @@ async function tryOnEditContent(e) {
 
   let comboMarkdownEditor : ComboMarkdownEditor;
 
-  const cancelAndReset = (e) => {
+  const cancelAndReset = (e: Event) => {
     e.preventDefault();
     showElem(renderContent);
     hideElem(editContentZone);
     comboMarkdownEditor.dropzoneReloadFiles();
   };
 
-  const saveAndRefresh = async (e) => {
+  const saveAndRefresh = async (e: Event) => {
     e.preventDefault();
     // we are already in a form, do not bubble up to the document otherwise there will be other "form submit handlers"
     // at the moment, the form submit event conflicts with initRepoDiffConversationForm (global '.conversation-holder form' event handler)
@@ -60,7 +60,7 @@ async function tryOnEditContent(e) {
       } else {
         renderContent.innerHTML = data.content;
         rawContent.textContent = comboMarkdownEditor.value();
-        const refIssues = renderContent.querySelectorAll('p .ref-issue');
+        const refIssues = renderContent.querySelectorAll<HTMLElement>('p .ref-issue');
         attachRefIssueContextPopup(refIssues);
       }
       const content = segment;
@@ -125,7 +125,7 @@ function extractSelectedMarkdown(container: HTMLElement) {
   return convertHtmlToMarkdown(el);
 }
 
-async function tryOnQuoteReply(e) {
+async function tryOnQuoteReply(e: Event) {
   const clickTarget = (e.target as HTMLElement).closest('.quote-reply');
   if (!clickTarget) return;
 
@@ -139,7 +139,7 @@ async function tryOnQuoteReply(e) {
 
   let editor;
   if (clickTarget.classList.contains('quote-reply-diff')) {
-    const replyBtn = clickTarget.closest('.comment-code-cloud').querySelector('button.comment-form-reply');
+    const replyBtn = clickTarget.closest('.comment-code-cloud').querySelector<HTMLElement>('button.comment-form-reply');
     editor = await handleReply(replyBtn);
   } else {
     // for normal issue/comment page
diff --git a/web_src/js/features/repo-issue-list.ts b/web_src/js/features/repo-issue-list.ts
index 74d4362bfd..01d4bb6f78 100644
--- a/web_src/js/features/repo-issue-list.ts
+++ b/web_src/js/features/repo-issue-list.ts
@@ -7,6 +7,7 @@ import {createSortable} from '../modules/sortable.ts';
 import {DELETE, POST} from '../modules/fetch.ts';
 import {parseDom} from '../utils.ts';
 import {fomanticQuery} from '../modules/fomantic/base.ts';
+import type {SortableEvent} from 'sortablejs';
 
 function initRepoIssueListCheckboxes() {
   const issueSelectAll = document.querySelector<HTMLInputElement>('.issue-checkbox-all');
@@ -104,7 +105,7 @@ function initDropdownUserRemoteSearch(el: Element) {
   $searchDropdown.dropdown('setting', {
     fullTextSearch: true,
     selectOnKeydown: false,
-    action: (_text, value) => {
+    action: (_text: string, value: string) => {
       window.location.href = actionJumpUrl.replace('{username}', encodeURIComponent(value));
     },
   });
@@ -133,7 +134,7 @@ function initDropdownUserRemoteSearch(el: Element) {
     $searchDropdown.dropdown('setting', 'apiSettings', {
       cache: false,
       url: `${searchUrl}&q={query}`,
-      onResponse(resp) {
+      onResponse(resp: any) {
         // the content is provided by backend IssuePosters handler
         processedResults.length = 0;
         for (const item of resp.results) {
@@ -153,7 +154,7 @@ function initDropdownUserRemoteSearch(el: Element) {
   const dropdownSetup = {...$searchDropdown.dropdown('internal', 'setup')};
   const dropdownTemplates = $searchDropdown.dropdown('setting', 'templates');
   $searchDropdown.dropdown('internal', 'setup', dropdownSetup);
-  dropdownSetup.menu = function (values) {
+  dropdownSetup.menu = function (values: any) {
     // remove old dynamic items
     for (const el of elMenu.querySelectorAll(':scope > .dynamic-item')) {
       el.remove();
@@ -193,7 +194,7 @@ function initPinRemoveButton() {
   }
 }
 
-async function pinMoveEnd(e) {
+async function pinMoveEnd(e: SortableEvent) {
   const url = e.item.getAttribute('data-move-url');
   const id = Number(e.item.getAttribute('data-issue-id'));
   await POST(url, {data: {id, position: e.newIndex + 1}});
diff --git a/web_src/js/features/repo-issue-sidebar-combolist.ts b/web_src/js/features/repo-issue-sidebar-combolist.ts
index 24d620547f..8db2f7665f 100644
--- a/web_src/js/features/repo-issue-sidebar-combolist.ts
+++ b/web_src/js/features/repo-issue-sidebar-combolist.ts
@@ -46,7 +46,7 @@ class IssueSidebarComboList {
     return Array.from(this.elDropdown.querySelectorAll('.menu > .item.checked'), (el) => el.getAttribute('data-value'));
   }
 
-  updateUiList(changedValues) {
+  updateUiList(changedValues: Array<string>) {
     const elEmptyTip = this.elList.querySelector('.item.empty-list');
     queryElemChildren(this.elList, '.item:not(.empty-list)', (el) => el.remove());
     for (const value of changedValues) {
@@ -60,7 +60,7 @@ class IssueSidebarComboList {
     toggleElem(elEmptyTip, !hasItems);
   }
 
-  async updateToBackend(changedValues) {
+  async updateToBackend(changedValues: Array<string>) {
     if (this.updateAlgo === 'diff') {
       for (const value of this.initialValues) {
         if (!changedValues.includes(value)) {
@@ -93,7 +93,7 @@ class IssueSidebarComboList {
     }
   }
 
-  async onItemClick(e) {
+  async onItemClick(e: Event) {
     const elItem = (e.target as HTMLElement).closest('.item');
     if (!elItem) return;
     e.preventDefault();
diff --git a/web_src/js/features/repo-issue.ts b/web_src/js/features/repo-issue.ts
index d74d3f7700..a0cb875a87 100644
--- a/web_src/js/features/repo-issue.ts
+++ b/web_src/js/features/repo-issue.ts
@@ -32,8 +32,8 @@ export function initRepoIssueSidebarList() {
     fullTextSearch: true,
     apiSettings: {
       url: issueSearchUrl,
-      onResponse(response) {
-        const filteredResponse = {success: true, results: []};
+      onResponse(response: any) {
+        const filteredResponse = {success: true, results: [] as Array<Record<string, any>>};
         const currIssueId = $('#new-dependency-drop-list').data('issue-id');
         // Parse the response from the api to work with our dropdown
         $.each(response, (_i, issue) => {
@@ -216,7 +216,7 @@ export function initRepoIssueCodeCommentCancel() {
 
 export function initRepoPullRequestUpdate() {
   // Pull Request update button
-  const pullUpdateButton = document.querySelector('.update-button > button');
+  const pullUpdateButton = document.querySelector<HTMLButtonElement>('.update-button > button');
   if (!pullUpdateButton) return;
 
   pullUpdateButton.addEventListener('click', async function (e) {
@@ -247,7 +247,7 @@ export function initRepoPullRequestUpdate() {
   });
 
   $('.update-button > .dropdown').dropdown({
-    onChange(_text, _value, $choice) {
+    onChange(_text: string, _value: string, $choice: any) {
       const choiceEl = $choice[0];
       const url = choiceEl.getAttribute('data-do');
       if (url) {
@@ -298,8 +298,8 @@ export function initRepoIssueReferenceRepositorySearch() {
     .dropdown({
       apiSettings: {
         url: `${appSubUrl}/repo/search?q={query}&limit=20`,
-        onResponse(response) {
-          const filteredResponse = {success: true, results: []};
+        onResponse(response: any) {
+          const filteredResponse = {success: true, results: [] as Array<Record<string, any>>};
           $.each(response.data, (_r, repo) => {
             filteredResponse.results.push({
               name: htmlEscape(repo.repository.full_name),
@@ -310,7 +310,7 @@ export function initRepoIssueReferenceRepositorySearch() {
         },
         cache: false,
       },
-      onChange(_value, _text, $choice) {
+      onChange(_value: string, _text: string, $choice: any) {
         const $form = $choice.closest('form');
         if (!$form.length) return;
 
@@ -360,7 +360,7 @@ export function initRepoIssueComments() {
   });
 }
 
-export async function handleReply(el) {
+export async function handleReply(el: HTMLElement) {
   const form = el.closest('.comment-code-cloud').querySelector('.comment-form');
   const textarea = form.querySelector('textarea');
 
@@ -379,7 +379,7 @@ export function initRepoPullRequestReview() {
       const groupID = commentDiv.closest('div[id^="code-comments-"]')?.getAttribute('id');
       if (groupID && groupID.startsWith('code-comments-')) {
         const id = groupID.slice(14);
-        const ancestorDiffBox = commentDiv.closest('.diff-file-box');
+        const ancestorDiffBox = commentDiv.closest<HTMLElement>('.diff-file-box');
 
         hideElem(`#show-outdated-${id}`);
         showElem(`#code-comments-${id}, #code-preview-${id}, #hide-outdated-${id}`);
@@ -589,7 +589,7 @@ export function initRepoIssueBranchSelect() {
   });
 }
 
-async function initSingleCommentEditor($commentForm) {
+async function initSingleCommentEditor($commentForm: any) {
   // pages:
   // * normal new issue/pr page: no status-button, no comment-button (there is only a normal submit button which can submit empty content)
   // * issue/pr view page: with comment form, has status-button and comment-button
@@ -611,7 +611,7 @@ async function initSingleCommentEditor($commentForm) {
   syncUiState();
 }
 
-function initIssueTemplateCommentEditors($commentForm) {
+function initIssueTemplateCommentEditors($commentForm: any) {
   // pages:
   // * new issue with issue template
   const $comboFields = $commentForm.find('.combo-editor-dropzone');
diff --git a/web_src/js/features/repo-migrate.ts b/web_src/js/features/repo-migrate.ts
index b75289feec..0788f83215 100644
--- a/web_src/js/features/repo-migrate.ts
+++ b/web_src/js/features/repo-migrate.ts
@@ -1,11 +1,11 @@
-import {hideElem, showElem} from '../utils/dom.ts';
+import {hideElem, showElem, type DOMEvent} from '../utils/dom.ts';
 import {GET, POST} from '../modules/fetch.ts';
 
 export function initRepoMigrationStatusChecker() {
   const repoMigrating = document.querySelector('#repo_migrating');
   if (!repoMigrating) return;
 
-  document.querySelector('#repo_migrating_retry')?.addEventListener('click', doMigrationRetry);
+  document.querySelector<HTMLButtonElement>('#repo_migrating_retry')?.addEventListener('click', doMigrationRetry);
 
   const repoLink = repoMigrating.getAttribute('data-migrating-repo-link');
 
@@ -55,7 +55,7 @@ export function initRepoMigrationStatusChecker() {
   syncTaskStatus(); // no await
 }
 
-async function doMigrationRetry(e) {
+async function doMigrationRetry(e: DOMEvent<MouseEvent>) {
   await POST(e.target.getAttribute('data-migrating-task-retry-url'));
   window.location.reload();
 }
diff --git a/web_src/js/features/repo-new.ts b/web_src/js/features/repo-new.ts
index 8a77a77b4a..f2c5eba62c 100644
--- a/web_src/js/features/repo-new.ts
+++ b/web_src/js/features/repo-new.ts
@@ -23,7 +23,7 @@ function initRepoNewTemplateSearch(form: HTMLFormElement) {
     $dropdown.dropdown('setting', {
       apiSettings: {
         url: `${appSubUrl}/repo/search?q={query}&template=true&priority_owner_id=${inputRepoOwnerUid.value}`,
-        onResponse(response) {
+        onResponse(response: any) {
           const results = [];
           results.push({name: '', value: ''}); // empty item means not using template
           for (const tmplRepo of response.data) {
@@ -66,7 +66,7 @@ export function initRepoNew() {
     let help = form.querySelector(`.help[data-help-for-repo-name="${CSS.escape(inputRepoName.value)}"]`);
     if (!help) help = form.querySelector(`.help[data-help-for-repo-name=""]`);
     showElem(help);
-    const repoNamePreferPrivate = {'.profile': false, '.profile-private': true};
+    const repoNamePreferPrivate: Record<string, boolean> = {'.profile': false, '.profile-private': true};
     const preferPrivate = repoNamePreferPrivate[inputRepoName.value];
     // inputPrivate might be disabled because site admin "force private"
     if (preferPrivate !== undefined && !inputPrivate.closest('.disabled, [disabled]')) {
diff --git a/web_src/js/features/repo-settings.ts b/web_src/js/features/repo-settings.ts
index 90b0219f3e..b61ef9a153 100644
--- a/web_src/js/features/repo-settings.ts
+++ b/web_src/js/features/repo-settings.ts
@@ -12,7 +12,7 @@ function initRepoSettingsCollaboration() {
   for (const dropdownEl of queryElems(document, '.page-content.repository .ui.dropdown.access-mode')) {
     const textEl = dropdownEl.querySelector(':scope > .text');
     $(dropdownEl).dropdown({
-      async action(text, value) {
+      async action(text: string, value: string) {
         dropdownEl.classList.add('is-loading', 'loading-icon-2px');
         const lastValue = dropdownEl.getAttribute('data-last-value');
         $(dropdownEl).dropdown('hide');
@@ -53,8 +53,8 @@ function initRepoSettingsSearchTeamBox() {
     apiSettings: {
       url: `${appSubUrl}/org/${searchTeamBox.getAttribute('data-org-name')}/teams/-/search?q={query}`,
       headers: {'X-Csrf-Token': csrfToken},
-      onResponse(response) {
-        const items = [];
+      onResponse(response: any) {
+        const items: Array<Record<string, any>> = [];
         $.each(response.data, (_i, item) => {
           items.push({
             title: item.name,
@@ -79,21 +79,21 @@ function initRepoSettingsGitHook() {
 function initRepoSettingsBranches() {
   if (!document.querySelector('.repository.settings.branches')) return;
 
-  for (const el of document.querySelectorAll('.toggle-target-enabled')) {
+  for (const el of document.querySelectorAll<HTMLInputElement>('.toggle-target-enabled')) {
     el.addEventListener('change', function () {
       const target = document.querySelector(this.getAttribute('data-target'));
       target?.classList.toggle('disabled', !this.checked);
     });
   }
 
-  for (const el of document.querySelectorAll('.toggle-target-disabled')) {
+  for (const el of document.querySelectorAll<HTMLInputElement>('.toggle-target-disabled')) {
     el.addEventListener('change', function () {
       const target = document.querySelector(this.getAttribute('data-target'));
       if (this.checked) target?.classList.add('disabled'); // only disable, do not auto enable
     });
   }
 
-  document.querySelector('#dismiss_stale_approvals')?.addEventListener('change', function () {
+  document.querySelector<HTMLInputElement>('#dismiss_stale_approvals')?.addEventListener('change', function () {
     document.querySelector('#ignore_stale_approvals_box')?.classList.toggle('disabled', this.checked);
   });
 
diff --git a/web_src/js/features/repo-wiki.ts b/web_src/js/features/repo-wiki.ts
index 484c628f9f..9ffa8a3275 100644
--- a/web_src/js/features/repo-wiki.ts
+++ b/web_src/js/features/repo-wiki.ts
@@ -70,7 +70,7 @@ async function initRepoWikiFormEditor() {
   });
 }
 
-function collapseWikiTocForMobile(collapse) {
+function collapseWikiTocForMobile(collapse: boolean) {
   if (collapse) {
     document.querySelector('.wiki-content-toc details')?.removeAttribute('open');
   }
diff --git a/web_src/js/features/sshkey-helper.ts b/web_src/js/features/sshkey-helper.ts
index 9234e3ec44..860bc5b294 100644
--- a/web_src/js/features/sshkey-helper.ts
+++ b/web_src/js/features/sshkey-helper.ts
@@ -1,6 +1,6 @@
 export function initSshKeyFormParser() {
   // Parse SSH Key
-  document.querySelector('#ssh-key-content')?.addEventListener('input', function () {
+  document.querySelector<HTMLTextAreaElement>('#ssh-key-content')?.addEventListener('input', function () {
     const arrays = this.value.split(' ');
     const title = document.querySelector<HTMLInputElement>('#ssh-key-title');
     if (!title.value && arrays.length === 3 && arrays[2] !== '') {
diff --git a/web_src/js/features/stopwatch.ts b/web_src/js/features/stopwatch.ts
index af52be4e24..a5cd5ae7c4 100644
--- a/web_src/js/features/stopwatch.ts
+++ b/web_src/js/features/stopwatch.ts
@@ -1,6 +1,6 @@
 import {createTippy} from '../modules/tippy.ts';
 import {GET} from '../modules/fetch.ts';
-import {hideElem, showElem} from '../utils/dom.ts';
+import {hideElem, queryElems, showElem} from '../utils/dom.ts';
 import {logoutFromWorker} from '../modules/worker.ts';
 
 const {appSubUrl, notificationSettings, enableTimeTracking, assetVersionEncoded} = window.config;
@@ -38,7 +38,7 @@ export function initStopwatch() {
   }
 
   let usingPeriodicPoller = false;
-  const startPeriodicPoller = (timeout) => {
+  const startPeriodicPoller = (timeout: number) => {
     if (timeout <= 0 || !Number.isFinite(timeout)) return;
     usingPeriodicPoller = true;
     setTimeout(() => updateStopwatchWithCallback(startPeriodicPoller, timeout), timeout);
@@ -103,7 +103,7 @@ export function initStopwatch() {
   startPeriodicPoller(notificationSettings.MinTimeout);
 }
 
-async function updateStopwatchWithCallback(callback, timeout) {
+async function updateStopwatchWithCallback(callback: (timeout: number) => void, timeout: number) {
   const isSet = await updateStopwatch();
 
   if (!isSet) {
@@ -125,7 +125,7 @@ async function updateStopwatch() {
   return updateStopwatchData(data);
 }
 
-function updateStopwatchData(data) {
+function updateStopwatchData(data: any) {
   const watch = data[0];
   const btnEls = document.querySelectorAll('.active-stopwatch');
   if (!watch) {
@@ -144,23 +144,10 @@ function updateStopwatchData(data) {
   return Boolean(data.length);
 }
 
-// TODO: This flickers on page load, we could avoid this by making a custom
-// element to render time periods. Feeding a datetime in backend does not work
-// when time zone between server and client differs.
-function updateStopwatchTime(seconds) {
-  if (!Number.isFinite(seconds)) return;
-  const datetime = (new Date(Date.now() - seconds * 1000)).toISOString();
-  for (const parent of document.querySelectorAll('.header-stopwatch-dot')) {
-    const existing = parent.querySelector(':scope > relative-time');
-    if (existing) {
-      existing.setAttribute('datetime', datetime);
-    } else {
-      const el = document.createElement('relative-time');
-      el.setAttribute('format', 'micro');
-      el.setAttribute('datetime', datetime);
-      el.setAttribute('lang', 'en-US');
-      el.setAttribute('title', ''); // make <relative-time> show no title and therefor no tooltip
-      parent.append(el);
-    }
-  }
+// TODO: This flickers on page load, we could avoid this by making a custom element to render time periods.
+function updateStopwatchTime(seconds: number) {
+  const hours = seconds / 3600 || 0;
+  const minutes = seconds / 60 || 0;
+  const timeText = hours >= 1 ? `${Math.round(hours)}h` : `${Math.round(minutes)}m`;
+  queryElems(document, '.header-stopwatch-dot', (el) => el.textContent = timeText);
 }
diff --git a/web_src/js/features/tablesort.ts b/web_src/js/features/tablesort.ts
index 15ea358fa3..0648ffd067 100644
--- a/web_src/js/features/tablesort.ts
+++ b/web_src/js/features/tablesort.ts
@@ -9,7 +9,7 @@ export function initTableSort() {
   }
 }
 
-function tableSort(normSort, revSort, isDefault) {
+function tableSort(normSort: string, revSort: string, isDefault: string) {
   if (!normSort) return false;
   if (!revSort) revSort = '';
 
diff --git a/web_src/js/features/tribute.ts b/web_src/js/features/tribute.ts
index fa65bcbb28..de1c3e97cd 100644
--- a/web_src/js/features/tribute.ts
+++ b/web_src/js/features/tribute.ts
@@ -1,14 +1,16 @@
 import {emojiKeys, emojiHTML, emojiString} from './emoji.ts';
 import {htmlEscape} from 'escape-goat';
 
-function makeCollections({mentions, emoji}) {
-  const collections = [];
+type TributeItem = Record<string, any>;
 
-  if (emoji) {
-    collections.push({
+export async function attachTribute(element: HTMLElement) {
+  const {default: Tribute} = await import(/* webpackChunkName: "tribute" */'tributejs');
+
+  const collections = [
+    { // emojis
       trigger: ':',
       requireLeadingSpace: true,
-      values: (query, cb) => {
+      values: (query: string, cb: (matches: Array<string>) => void) => {
         const matches = [];
         for (const name of emojiKeys) {
           if (name.includes(query)) {
@@ -18,22 +20,18 @@ function makeCollections({mentions, emoji}) {
         }
         cb(matches);
       },
-      lookup: (item) => item,
-      selectTemplate: (item) => {
+      lookup: (item: TributeItem) => item,
+      selectTemplate: (item: TributeItem) => {
         if (item === undefined) return null;
         return emojiString(item.original);
       },
-      menuItemTemplate: (item) => {
+      menuItemTemplate: (item: TributeItem) => {
         return `<div class="tribute-item">${emojiHTML(item.original)}<span>${htmlEscape(item.original)}</span></div>`;
       },
-    });
-  }
-
-  if (mentions) {
-    collections.push({
+    }, { // mentions
       values: window.config.mentionValues ?? [],
       requireLeadingSpace: true,
-      menuItemTemplate: (item) => {
+      menuItemTemplate: (item: TributeItem) => {
         return `
           <div class="tribute-item">
             <img src="${htmlEscape(item.original.avatar)}" width="21" height="21"/>
@@ -42,15 +40,9 @@ function makeCollections({mentions, emoji}) {
           </div>
         `;
       },
-    });
-  }
+    },
+  ];
 
-  return collections;
-}
-
-export async function attachTribute(element, {mentions, emoji}) {
-  const {default: Tribute} = await import(/* webpackChunkName: "tribute" */'tributejs');
-  const collections = makeCollections({mentions, emoji});
   // @ts-expect-error TS2351: This expression is not constructable (strange, why)
   const tribute = new Tribute({collection: collections, noMatchTemplate: ''});
   tribute.attach(element);
diff --git a/web_src/js/features/user-auth-webauthn.ts b/web_src/js/features/user-auth-webauthn.ts
index 70516c280d..b9ab2e2088 100644
--- a/web_src/js/features/user-auth-webauthn.ts
+++ b/web_src/js/features/user-auth-webauthn.ts
@@ -114,7 +114,7 @@ async function login2FA() {
   }
 }
 
-async function verifyAssertion(assertedCredential) {
+async function verifyAssertion(assertedCredential: any) { // TODO: Credential type does not work
   // Move data into Arrays in case it is super long
   const authData = new Uint8Array(assertedCredential.response.authenticatorData);
   const clientDataJSON = new Uint8Array(assertedCredential.response.clientDataJSON);
@@ -148,7 +148,7 @@ async function verifyAssertion(assertedCredential) {
   window.location.href = reply?.redirect ?? `${appSubUrl}/`;
 }
 
-async function webauthnRegistered(newCredential) {
+async function webauthnRegistered(newCredential: any) { // TODO: Credential type does not work
   const attestationObject = new Uint8Array(newCredential.response.attestationObject);
   const clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON);
   const rawId = new Uint8Array(newCredential.rawId);
diff --git a/web_src/js/features/user-settings.ts b/web_src/js/features/user-settings.ts
index c097df7b6c..6312a8b682 100644
--- a/web_src/js/features/user-settings.ts
+++ b/web_src/js/features/user-settings.ts
@@ -13,7 +13,7 @@ export function initUserSettings() {
 
   initUserSettingsAvatarCropper();
 
-  const usernameInput = document.querySelector('#username');
+  const usernameInput = document.querySelector<HTMLInputElement>('#username');
   if (!usernameInput) return;
   usernameInput.addEventListener('input', function () {
     const prompt = document.querySelector('#name-change-prompt');
diff --git a/web_src/js/index.ts b/web_src/js/index.ts
index 022be033da..b89e596047 100644
--- a/web_src/js/index.ts
+++ b/web_src/js/index.ts
@@ -2,8 +2,7 @@
 import './bootstrap.ts';
 import './htmx.ts';
 
-import {initDashboardRepoList} from './components/DashboardRepoList.vue';
-
+import {initDashboardRepoList} from './features/dashboard.ts';
 import {initGlobalCopyToClipboardListener} from './features/clipboard.ts';
 import {initContextPopups} from './features/contextpopup.ts';
 import {initRepoGraphGit} from './features/repo-graph.ts';
@@ -53,7 +52,7 @@ import {initRepoWikiForm} from './features/repo-wiki.ts';
 import {initRepository, initBranchSelectorTabs} from './features/repo-legacy.ts';
 import {initCopyContent} from './features/copycontent.ts';
 import {initCaptcha} from './features/captcha.ts';
-import {initRepositoryActionView} from './components/RepoActionView.vue';
+import {initRepositoryActionView} from './features/repo-actions.ts';
 import {initGlobalTooltips} from './modules/tippy.ts';
 import {initGiteaFomantic} from './modules/fomantic.ts';
 import {initSubmitEventPolyfill, onDomReady} from './utils/dom.ts';
diff --git a/web_src/js/markup/asciicast.ts b/web_src/js/markup/asciicast.ts
index 97b18743a1..9baae6ba85 100644
--- a/web_src/js/markup/asciicast.ts
+++ b/web_src/js/markup/asciicast.ts
@@ -3,6 +3,7 @@ export async function renderAsciicast() {
   if (!els.length) return;
 
   const [player] = await Promise.all([
+    // @ts-expect-error: module exports no types
     import(/* webpackChunkName: "asciinema-player" */'asciinema-player'),
     import(/* webpackChunkName: "asciinema-player" */'asciinema-player/dist/bundle/asciinema-player.css'),
   ]);
diff --git a/web_src/js/markup/html2markdown.ts b/web_src/js/markup/html2markdown.ts
index fc2083e86d..8c2d2f8c86 100644
--- a/web_src/js/markup/html2markdown.ts
+++ b/web_src/js/markup/html2markdown.ts
@@ -1,7 +1,9 @@
 import {htmlEscape} from 'escape-goat';
 
+type Processor = (el: HTMLElement) => string | HTMLElement | void;
+
 type Processors = {
-  [tagName: string]: (el: HTMLElement) => string | HTMLElement | void;
+  [tagName: string]: Processor;
 }
 
 type ProcessorContext = {
@@ -11,7 +13,7 @@ type ProcessorContext = {
 }
 
 function prepareProcessors(ctx:ProcessorContext): Processors {
-  const processors = {
+  const processors: Processors = {
     H1(el: HTMLElement) {
       const level = parseInt(el.tagName.slice(1));
       el.textContent = `${'#'.repeat(level)} ${el.textContent.trim()}`;
diff --git a/web_src/js/modules/fomantic/dimmer.ts b/web_src/js/modules/fomantic/dimmer.ts
index 4e05cac0cd..cbdfac23cb 100644
--- a/web_src/js/modules/fomantic/dimmer.ts
+++ b/web_src/js/modules/fomantic/dimmer.ts
@@ -3,7 +3,7 @@ import {queryElemChildren} from '../../utils/dom.ts';
 
 export function initFomanticDimmer() {
   // stand-in for removed dimmer module
-  $.fn.dimmer = function (arg0: string, arg1: any) {
+  $.fn.dimmer = function (this: any, arg0: string, arg1: any) {
     if (arg0 === 'add content') {
       const $el = arg1;
       const existingDimmer = document.querySelector('body > .ui.dimmer');
diff --git a/web_src/js/modules/fomantic/dropdown.ts b/web_src/js/modules/fomantic/dropdown.ts
index 9bdc9bfc33..8736e041df 100644
--- a/web_src/js/modules/fomantic/dropdown.ts
+++ b/web_src/js/modules/fomantic/dropdown.ts
@@ -17,7 +17,7 @@ export function initAriaDropdownPatch() {
 // the patched `$.fn.dropdown` function, it passes the arguments to Fomantic's `$.fn.dropdown` function, and:
 // * it does the one-time attaching on the first call
 // * it delegates the `onLabelCreate` to the patched `onLabelCreate` to add necessary aria attributes
-function ariaDropdownFn(...args: Parameters<FomanticInitFunction>) {
+function ariaDropdownFn(this: any, ...args: Parameters<FomanticInitFunction>) {
   const ret = fomanticDropdownFn.apply(this, args);
 
   // if the `$().dropdown()` call is without arguments, or it has non-string (object) argument,
@@ -38,7 +38,7 @@ function ariaDropdownFn(...args: Parameters<FomanticInitFunction>) {
 // the elements inside the dropdown menu item should not be focusable, the focus should always be on the dropdown primary element.
 function updateMenuItem(dropdown: HTMLElement, item: HTMLElement) {
   if (!item.id) item.id = generateAriaId();
-  item.setAttribute('role', dropdown[ariaPatchKey].listItemRole);
+  item.setAttribute('role', (dropdown as any)[ariaPatchKey].listItemRole);
   item.setAttribute('tabindex', '-1');
   for (const el of item.querySelectorAll('a, input, button')) el.setAttribute('tabindex', '-1');
 }
@@ -61,7 +61,7 @@ function updateSelectionLabel(label: HTMLElement) {
   }
 }
 
-function processMenuItems($dropdown, dropdownCall) {
+function processMenuItems($dropdown: any, dropdownCall: any) {
   const hideEmptyDividers = dropdownCall('setting', 'hideDividers') === 'empty';
   const itemsMenu = $dropdown[0].querySelector('.scrolling.menu') || $dropdown[0].querySelector('.menu');
   if (hideEmptyDividers) hideScopedEmptyDividers(itemsMenu);
@@ -76,18 +76,18 @@ function delegateOne($dropdown: any) {
   const oldFocusSearch = dropdownCall('internal', 'focusSearch');
   const oldBlurSearch = dropdownCall('internal', 'blurSearch');
   // * If the "dropdown icon" is clicked, Fomantic calls "focusSearch", so show the menu
-  dropdownCall('internal', 'focusSearch', function () { dropdownCall('show'); oldFocusSearch.call(this) });
+  dropdownCall('internal', 'focusSearch', function (this: any) { dropdownCall('show'); oldFocusSearch.call(this) });
   // * If the "dropdown icon" is clicked again when the menu is visible, Fomantic calls "blurSearch", so hide the menu
-  dropdownCall('internal', 'blurSearch', function () { oldBlurSearch.call(this); dropdownCall('hide') });
+  dropdownCall('internal', 'blurSearch', function (this: any) { oldBlurSearch.call(this); dropdownCall('hide') });
 
   const oldFilterItems = dropdownCall('internal', 'filterItems');
-  dropdownCall('internal', 'filterItems', function (...args: any[]) {
+  dropdownCall('internal', 'filterItems', function (this: any, ...args: any[]) {
     oldFilterItems.call(this, ...args);
     processMenuItems($dropdown, dropdownCall);
   });
 
   const oldShow = dropdownCall('internal', 'show');
-  dropdownCall('internal', 'show', function (...args: any[]) {
+  dropdownCall('internal', 'show', function (this: any, ...args: any[]) {
     oldShow.call(this, ...args);
     processMenuItems($dropdown, dropdownCall);
   });
@@ -110,7 +110,7 @@ function delegateOne($dropdown: any) {
 
   // the `onLabelCreate` is used to add necessary aria attributes for dynamically created selection labels
   const dropdownOnLabelCreateOld = dropdownCall('setting', 'onLabelCreate');
-  dropdownCall('setting', 'onLabelCreate', function(value: any, text: string) {
+  dropdownCall('setting', 'onLabelCreate', function(this: any, value: any, text: string) {
     const $label = dropdownOnLabelCreateOld.call(this, value, text);
     updateSelectionLabel($label[0]);
     return $label;
@@ -143,7 +143,7 @@ function attachStaticElements(dropdown: HTMLElement, focusable: HTMLElement, men
   $(menu).find('> .item').each((_, item) => updateMenuItem(dropdown, item));
 
   // this role could only be changed after its content is ready, otherwise some browsers+readers (like Chrome+AppleVoice) crash
-  menu.setAttribute('role', dropdown[ariaPatchKey].listPopupRole);
+  menu.setAttribute('role', (dropdown as any)[ariaPatchKey].listPopupRole);
 
   // prepare selection label items
   for (const label of dropdown.querySelectorAll<HTMLElement>('.ui.label')) {
@@ -151,8 +151,8 @@ function attachStaticElements(dropdown: HTMLElement, focusable: HTMLElement, men
   }
 
   // make the primary element (focusable) aria-friendly
-  focusable.setAttribute('role', focusable.getAttribute('role') ?? dropdown[ariaPatchKey].focusableRole);
-  focusable.setAttribute('aria-haspopup', dropdown[ariaPatchKey].listPopupRole);
+  focusable.setAttribute('role', focusable.getAttribute('role') ?? (dropdown as any)[ariaPatchKey].focusableRole);
+  focusable.setAttribute('aria-haspopup', (dropdown as any)[ariaPatchKey].listPopupRole);
   focusable.setAttribute('aria-controls', menu.id);
   focusable.setAttribute('aria-expanded', 'false');
 
@@ -164,7 +164,7 @@ function attachStaticElements(dropdown: HTMLElement, focusable: HTMLElement, men
 }
 
 function attachInit(dropdown: HTMLElement) {
-  dropdown[ariaPatchKey] = {};
+  (dropdown as any)[ariaPatchKey] = {};
   if (dropdown.classList.contains('custom')) return;
 
   // Dropdown has 2 different focusing behaviors
@@ -204,9 +204,9 @@ function attachInit(dropdown: HTMLElement) {
   // Since #19861 we have prepared the "combobox" solution, but didn't get enough time to put it into practice and test before.
   const isComboBox = dropdown.querySelectorAll('input').length > 0;
 
-  dropdown[ariaPatchKey].focusableRole = isComboBox ? 'combobox' : 'menu';
-  dropdown[ariaPatchKey].listPopupRole = isComboBox ? 'listbox' : '';
-  dropdown[ariaPatchKey].listItemRole = isComboBox ? 'option' : 'menuitem';
+  (dropdown as any)[ariaPatchKey].focusableRole = isComboBox ? 'combobox' : 'menu';
+  (dropdown as any)[ariaPatchKey].listPopupRole = isComboBox ? 'listbox' : '';
+  (dropdown as any)[ariaPatchKey].listItemRole = isComboBox ? 'option' : 'menuitem';
 
   attachDomEvents(dropdown, focusable, menu);
   attachStaticElements(dropdown, focusable, menu);
@@ -229,7 +229,7 @@ function attachDomEvents(dropdown: HTMLElement, focusable: HTMLElement, menu: HT
     // if the popup is visible and has an active/selected item, use its id as aria-activedescendant
     if (menuVisible) {
       focusable.setAttribute('aria-activedescendant', active.id);
-    } else if (dropdown[ariaPatchKey].listPopupRole === 'menu') {
+    } else if ((dropdown as any)[ariaPatchKey].listPopupRole === 'menu') {
       // for menu, when the popup is hidden, no need to keep the aria-activedescendant, and clear the active/selected item
       focusable.removeAttribute('aria-activedescendant');
       active.classList.remove('active', 'selected');
@@ -253,7 +253,7 @@ function attachDomEvents(dropdown: HTMLElement, focusable: HTMLElement, menu: HT
   // when the popup is hiding, it's better to have a small "delay", because there is a Fomantic UI animation
   // without the delay for hiding, the UI will be somewhat laggy and sometimes may get stuck in the animation.
   const deferredRefreshAriaActiveItem = (delay = 0) => { setTimeout(refreshAriaActiveItem, delay) };
-  dropdown[ariaPatchKey].deferredRefreshAriaActiveItem = deferredRefreshAriaActiveItem;
+  (dropdown as any)[ariaPatchKey].deferredRefreshAriaActiveItem = deferredRefreshAriaActiveItem;
   dropdown.addEventListener('keyup', (e) => { if (e.key.startsWith('Arrow')) deferredRefreshAriaActiveItem(); });
 
   // if the dropdown has been opened by focus, do not trigger the next click event again.
@@ -363,7 +363,7 @@ function onResponseKeepSelectedItem(dropdown: typeof $|HTMLElement, selectedValu
   // then the dropdown only shows other items and will select another (wrong) one.
   // It can't be easily fix by using setTimeout(patch, 0) in `onResponse` because the `onResponse` is called before another `setTimeout(..., timeLeft)`
   // Fortunately, the "timeLeft" is controlled by "loadingDuration" which is always zero at the moment, so we can use `setTimeout(..., 10)`
-  const elDropdown = (dropdown instanceof HTMLElement) ? dropdown : dropdown[0];
+  const elDropdown = (dropdown instanceof HTMLElement) ? dropdown : (dropdown as any)[0];
   setTimeout(() => {
     queryElems(elDropdown, `.menu .item[data-value="${CSS.escape(selectedValue)}"].filtered`, (el) => el.classList.remove('filtered'));
     $(elDropdown).dropdown('set selected', selectedValue ?? '');
diff --git a/web_src/js/modules/fomantic/modal.ts b/web_src/js/modules/fomantic/modal.ts
index fb80047d01..6a2c558890 100644
--- a/web_src/js/modules/fomantic/modal.ts
+++ b/web_src/js/modules/fomantic/modal.ts
@@ -12,7 +12,7 @@ export function initAriaModalPatch() {
 
 // the patched `$.fn.modal` modal function
 // * it does the one-time attaching on the first call
-function ariaModalFn(...args: Parameters<FomanticInitFunction>) {
+function ariaModalFn(this: any, ...args: Parameters<FomanticInitFunction>) {
   const ret = fomanticModalFn.apply(this, args);
   if (args[0] === 'show' || args[0]?.autoShow) {
     for (const el of this) {
diff --git a/web_src/js/modules/tippy.ts b/web_src/js/modules/tippy.ts
index aaaf580de1..bc6d5bfdd6 100644
--- a/web_src/js/modules/tippy.ts
+++ b/web_src/js/modules/tippy.ts
@@ -121,7 +121,7 @@ function switchTitleToTooltip(target: Element): void {
  * Some browsers like PaleMoon don't support "addEventListener('mouseenter', capture)"
  * The tippy by default uses "mouseenter" event to show, so we use "mouseover" event to switch to tippy
  */
-function lazyTooltipOnMouseHover(e: Event): void {
+function lazyTooltipOnMouseHover(this: HTMLElement, e: Event): void {
   e.target.removeEventListener('mouseover', lazyTooltipOnMouseHover, true);
   attachTooltip(this);
 }
diff --git a/web_src/js/standalone/devtest.ts b/web_src/js/standalone/devtest.ts
index 3489697a2f..e6baf6c9ce 100644
--- a/web_src/js/standalone/devtest.ts
+++ b/web_src/js/standalone/devtest.ts
@@ -1,7 +1,7 @@
 import {showInfoToast, showWarningToast, showErrorToast} from '../modules/toast.ts';
 
 function initDevtestToast() {
-  const levelMap = {info: showInfoToast, warning: showWarningToast, error: showErrorToast};
+  const levelMap: Record<string, any> = {info: showInfoToast, warning: showWarningToast, error: showErrorToast};
   for (const el of document.querySelectorAll('.toast-test-button')) {
     el.addEventListener('click', () => {
       const level = el.getAttribute('data-toast-level');
diff --git a/web_src/js/svg.ts b/web_src/js/svg.ts
index 6a8246fa1b..b193afb255 100644
--- a/web_src/js/svg.ts
+++ b/web_src/js/svg.ts
@@ -1,4 +1,4 @@
-import {h} from 'vue';
+import {defineComponent, h, type PropType} from 'vue';
 import {parseDom, serializeXml} from './utils.ts';
 import giteaDoubleChevronLeft from '../../public/assets/img/svg/gitea-double-chevron-left.svg';
 import giteaDoubleChevronRight from '../../public/assets/img/svg/gitea-double-chevron-right.svg';
@@ -35,6 +35,7 @@ import octiconGitBranch from '../../public/assets/img/svg/octicon-git-branch.svg
 import octiconGitCommit from '../../public/assets/img/svg/octicon-git-commit.svg';
 import octiconGitMerge from '../../public/assets/img/svg/octicon-git-merge.svg';
 import octiconGitPullRequest from '../../public/assets/img/svg/octicon-git-pull-request.svg';
+import octiconGitPullRequestClosed from '../../public/assets/img/svg/octicon-git-pull-request-closed.svg';
 import octiconGitPullRequestDraft from '../../public/assets/img/svg/octicon-git-pull-request-draft.svg';
 import octiconGrabber from '../../public/assets/img/svg/octicon-grabber.svg';
 import octiconHeading from '../../public/assets/img/svg/octicon-heading.svg';
@@ -112,6 +113,7 @@ const svgs = {
   'octicon-git-commit': octiconGitCommit,
   'octicon-git-merge': octiconGitMerge,
   'octicon-git-pull-request': octiconGitPullRequest,
+  'octicon-git-pull-request-closed': octiconGitPullRequestClosed,
   'octicon-git-pull-request-draft': octiconGitPullRequestDraft,
   'octicon-grabber': octiconGrabber,
   'octicon-heading': octiconHeading,
@@ -194,10 +196,10 @@ export function svgParseOuterInner(name: SvgName) {
   return {svgOuter, svgInnerHtml};
 }
 
-export const SvgIcon = {
+export const SvgIcon = defineComponent({
   name: 'SvgIcon',
   props: {
-    name: {type: String, required: true},
+    name: {type: String as PropType<SvgName>, required: true},
     size: {type: Number, default: 16},
     className: {type: String, default: ''},
     symbolId: {type: String},
@@ -206,7 +208,7 @@ export const SvgIcon = {
     let {svgOuter, svgInnerHtml} = svgParseOuterInner(this.name);
     // https://vuejs.org/guide/extras/render-function.html#creating-vnodes
     // the `^` is used for attr, set SVG attributes like 'width', `aria-hidden`, `viewBox`, etc
-    const attrs = {};
+    const attrs: Record<string, any> = {};
     for (const attr of svgOuter.attributes) {
       if (attr.name === 'class') continue;
       attrs[`^${attr.name}`] = attr.value;
@@ -215,7 +217,7 @@ export const SvgIcon = {
     attrs[`^height`] = this.size;
 
     // make the <SvgIcon class="foo" class-name="bar"> classes work together
-    const classes = [];
+    const classes: Array<string> = [];
     for (const cls of svgOuter.classList) {
       classes.push(cls);
     }
@@ -234,4 +236,4 @@ export const SvgIcon = {
       innerHTML: svgInnerHtml,
     });
   },
-};
+});
diff --git a/web_src/js/types.ts b/web_src/js/types.ts
index e7c9ac0df4..1b5e652f66 100644
--- a/web_src/js/types.ts
+++ b/web_src/js/types.ts
@@ -22,6 +22,8 @@ export type Config = {
   i18n: Record<string, string>,
 }
 
+export type IntervalId = ReturnType<typeof setInterval>;
+
 export type Intent = 'error' | 'warning' | 'info';
 
 export type RequestData = string | FormData | URLSearchParams | Record<string, any>;
@@ -30,6 +32,11 @@ export type RequestOpts = {
   data?: RequestData,
 } & RequestInit;
 
+export type RepoOwnerPathInfo = {
+  ownerName: string,
+  repoName: string,
+}
+
 export type IssuePathInfo = {
   ownerName: string,
   repoName: string,
diff --git a/web_src/js/utils.test.ts b/web_src/js/utils.test.ts
index b527111533..ccdbc2dbd7 100644
--- a/web_src/js/utils.test.ts
+++ b/web_src/js/utils.test.ts
@@ -1,7 +1,7 @@
 import {
   basename, extname, isObject, stripTags, parseIssueHref,
   parseUrl, translateMonth, translateDay, blobToDataURI,
-  toAbsoluteUrl, encodeURLEncodedBase64, decodeURLEncodedBase64, isImageFile, isVideoFile, parseIssueNewHref,
+  toAbsoluteUrl, encodeURLEncodedBase64, decodeURLEncodedBase64, isImageFile, isVideoFile, parseRepoOwnerPathInfo,
 } from './utils.ts';
 
 test('basename', () => {
@@ -45,12 +45,14 @@ test('parseIssueHref', () => {
   expect(parseIssueHref('')).toEqual({ownerName: undefined, repoName: undefined, type: undefined, index: undefined});
 });
 
-test('parseIssueNewHref', () => {
-  expect(parseIssueNewHref('/owner/repo/issues/new')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues'});
-  expect(parseIssueNewHref('/owner/repo/issues/new?query')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues'});
-  expect(parseIssueNewHref('/sub/owner/repo/issues/new#hash')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues'});
-  expect(parseIssueNewHref('/sub/owner/repo/compare/feature/branch-1...fix/branch-2')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'pulls'});
-  expect(parseIssueNewHref('/other')).toEqual({});
+test('parseRepoOwnerPathInfo', () => {
+  expect(parseRepoOwnerPathInfo('/owner/repo/issues/new')).toEqual({ownerName: 'owner', repoName: 'repo'});
+  expect(parseRepoOwnerPathInfo('/owner/repo/releases')).toEqual({ownerName: 'owner', repoName: 'repo'});
+  expect(parseRepoOwnerPathInfo('/other')).toEqual({});
+  window.config.appSubUrl = '/sub';
+  expect(parseRepoOwnerPathInfo('/sub/owner/repo/issues/new')).toEqual({ownerName: 'owner', repoName: 'repo'});
+  expect(parseRepoOwnerPathInfo('/sub/owner/repo/compare/feature/branch-1...fix/branch-2')).toEqual({ownerName: 'owner', repoName: 'repo'});
+  window.config.appSubUrl = '';
 });
 
 test('parseUrl', () => {
diff --git a/web_src/js/utils.ts b/web_src/js/utils.ts
index 2a2bdc60f9..54f59a2c03 100644
--- a/web_src/js/utils.ts
+++ b/web_src/js/utils.ts
@@ -1,5 +1,5 @@
 import {decode, encode} from 'uint8-to-base64';
-import type {IssuePageInfo, IssuePathInfo} from './types.ts';
+import type {IssuePageInfo, IssuePathInfo, RepoOwnerPathInfo} from './types.ts';
 
 // transform /path/to/file.ext to file.ext
 export function basename(path: string): string {
@@ -32,16 +32,17 @@ export function stripTags(text: string): string {
 }
 
 export function parseIssueHref(href: string): IssuePathInfo {
+  // FIXME: it should use pathname and trim the appSubUrl ahead
   const path = (href || '').replace(/[#?].*$/, '');
   const [_, ownerName, repoName, pathType, indexString] = /([^/]+)\/([^/]+)\/(issues|pulls)\/([0-9]+)/.exec(path) || [];
   return {ownerName, repoName, pathType, indexString};
 }
 
-export function parseIssueNewHref(href: string): IssuePathInfo {
-  const path = (href || '').replace(/[#?].*$/, '');
-  const [_, ownerName, repoName, pathTypeField] = /([^/]+)\/([^/]+)\/(issues\/new|compare\/.+\.\.\.)/.exec(path) || [];
-  const pathType = pathTypeField ? (pathTypeField.startsWith('issues/new') ? 'issues' : 'pulls') : undefined;
-  return {ownerName, repoName, pathType};
+export function parseRepoOwnerPathInfo(pathname: string): RepoOwnerPathInfo {
+  const appSubUrl = window.config.appSubUrl;
+  if (appSubUrl && pathname.startsWith(appSubUrl)) pathname = pathname.substring(appSubUrl.length);
+  const [_, ownerName, repoName] = /([^/]+)\/([^/]+)/.exec(pathname) || [];
+  return {ownerName, repoName};
 }
 
 export function parseIssuePageInfo(): IssuePageInfo {
@@ -165,10 +166,10 @@ export function sleep(ms: number): Promise<void> {
   return new Promise((resolve) => setTimeout(resolve, ms));
 }
 
-export function isImageFile({name, type}: {name: string, type?: string}): boolean {
+export function isImageFile({name, type}: {name?: string, type?: string}): boolean {
   return /\.(avif|jpe?g|png|gif|webp|svg|heic)$/i.test(name || '') || type?.startsWith('image/');
 }
 
-export function isVideoFile({name, type}: {name: string, type?: string}): boolean {
+export function isVideoFile({name, type}: {name?: string, type?: string}): boolean {
   return /\.(mpe?g|mp4|mkv|webm)$/i.test(name || '') || type?.startsWith('video/');
 }
diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts
index e24cb29bac..603f967b34 100644
--- a/web_src/js/utils/dom.ts
+++ b/web_src/js/utils/dom.ts
@@ -255,12 +255,12 @@ export function loadElem(el: LoadableElement, src: string) {
 // it can't use other transparent polyfill patches because PaleMoon also doesn't support "addEventListener(capture)"
 const needSubmitEventPolyfill = typeof SubmitEvent === 'undefined';
 
-export function submitEventSubmitter(e) {
+export function submitEventSubmitter(e: any) {
   e = e.originalEvent ?? e; // if the event is wrapped by jQuery, use "originalEvent", otherwise, use the event itself
   return needSubmitEventPolyfill ? (e.target._submitter || null) : e.submitter;
 }
 
-function submitEventPolyfillListener(e) {
+function submitEventPolyfillListener(e: DOMEvent<Event>) {
   const form = e.target.closest('form');
   if (!form) return;
   form._submitter = e.target.closest('button:not([type]), button[type="submit"], input[type="submit"]');
diff --git a/web_src/js/utils/image.test.ts b/web_src/js/utils/image.test.ts
index da0605f1d0..49856c891c 100644
--- a/web_src/js/utils/image.test.ts
+++ b/web_src/js/utils/image.test.ts
@@ -4,7 +4,7 @@ const pngNoPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA
 const pngPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAEElEQVQI12OQNZcAIgYIBQAL8gGxdzzM0A==';
 const pngEmpty = 'data:image/png;base64,';
 
-async function dataUriToBlob(datauri) {
+async function dataUriToBlob(datauri: string) {
   return await (await globalThis.fetch(datauri)).blob();
 }
 
diff --git a/web_src/js/utils/time.ts b/web_src/js/utils/time.ts
index 6951ebfedb..c63498345f 100644
--- a/web_src/js/utils/time.ts
+++ b/web_src/js/utils/time.ts
@@ -54,7 +54,7 @@ export type DayDataObject = {
 }
 
 export function fillEmptyStartDaysWithZeroes(startDays: number[], data: DayDataObject): DayData[] {
-  const result = {};
+  const result: Record<string, any> = {};
 
   for (const startDay of startDays) {
     result[startDay] = data[startDay] || {'week': startDay, 'additions': 0, 'deletions': 0, 'commits': 0};
diff --git a/web_src/js/webcomponents/absolute-date.ts b/web_src/js/webcomponents/absolute-date.ts
index 8eb1c3e37e..23a8606673 100644
--- a/web_src/js/webcomponents/absolute-date.ts
+++ b/web_src/js/webcomponents/absolute-date.ts
@@ -15,7 +15,7 @@ window.customElements.define('absolute-date', class extends HTMLElement {
   initialized = false;
 
   update = () => {
-    const opt: Intl.DateTimeFormatOptions = {};
+    const opt: Record<string, string> = {};
     for (const attr of ['year', 'month', 'weekday', 'day']) {
       if (this.getAttribute(attr)) opt[attr] = this.getAttribute(attr);
     }