Skip to content

Commit 5dee1de

Browse files
committed
feat: add support for suspending/resuming a feed
1 parent c217a2a commit 5dee1de

File tree

10 files changed

+114
-35
lines changed

10 files changed

+114
-35
lines changed

api/api.go

+8
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ func Run() {
2828
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
2929

3030
r := echo.New()
31+
32+
if conf.Debug {
33+
r.Debug = true
34+
r.Use(middleware.BodyDump(func(c echo.Context, reqBody, resBody []byte) {
35+
r.Logger.Debugf("req: %s\nresp: %s\n", reqBody, resBody)
36+
}))
37+
}
38+
3139
r.HideBanner = true
3240
r.HTTPErrorHandler = errorHandler
3341
r.Validator = newCustomValidator()

frontend/src/lib/api/feed.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,16 @@ export async function createFeed(data: {
3131
});
3232
}
3333

34-
export async function updateFeed(data: Feed) {
35-
return await api.patch('feeds/' + data.id, {
36-
json: { name: data.name, link: data.link, group_id: data.group.id }
34+
export type FeedUpdateForm = {
35+
name?: string;
36+
link?: string;
37+
suspended?: boolean;
38+
group_id?: number;
39+
};
40+
41+
export async function updateFeed(id: number, data: FeedUpdateForm) {
42+
return await api.patch('feeds/' + id, {
43+
json: data
3744
});
3845
}
3946

frontend/src/lib/api/model.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type Feed = {
99
link: string;
1010
failure: string;
1111
updated_at: Date;
12+
suspended: boolean;
1213
group: Group;
1314
};
1415

frontend/src/routes/feeds/ActionAdd.svelte

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
link: '',
2424
failure: '',
2525
updated_at: new Date(),
26+
suspended: false,
2627
group: { id: groups[0].id, name: groups[0].name }
2728
};
2829
}

frontend/src/routes/feeds/Detail.svelte

+62-18
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import { Label } from '$lib/components/ui/label';
88
import { AlertCircleIcon, Loader2Icon } from 'lucide-svelte';
99
import * as Alert from '$lib/components/ui/alert';
10-
import { deleteFeed, refreshFeeds, updateFeed } from '$lib/api/feed';
10+
import { deleteFeed, refreshFeeds, updateFeed, type FeedUpdateForm } from '$lib/api/feed';
1111
import type { Feed } from '$lib/api/model';
1212
import type { groupFeeds } from './+page';
1313
import { invalidateAll } from '$app/navigation';
@@ -18,12 +18,12 @@
1818
export let groups: groupFeeds[];
1919
export let show = false;
2020
export let selectedFeed: Feed;
21-
let formData: Feed;
21+
let formData: FeedUpdateForm;
2222
let refreshing = false;
2323
2424
$: {
2525
if (show) {
26-
formData = Object.assign({}, selectedFeed);
26+
formData = {};
2727
}
2828
}
2929
@@ -55,19 +55,29 @@
5555
}
5656
5757
async function handleUpdate() {
58-
if (!formData) return;
59-
toast.promise(updateFeed(formData), {
58+
toast.promise(updateFeed(selectedFeed.id, formData), {
6059
loading: 'Updating',
6160
success: () => {
6261
invalidateAll();
63-
return formData.name + ' has been updated';
62+
return 'Update successfully';
6463
},
6564
error: (e) => {
6665
invalidateAll();
6766
return (e as Error).message;
6867
}
6968
});
7069
}
70+
71+
async function handleToggleSuspended() {
72+
const data: FeedUpdateForm = { suspended: !selectedFeed.suspended };
73+
try {
74+
await updateFeed(selectedFeed.id, data);
75+
toast.success('Update successfully');
76+
invalidateAll();
77+
} catch (e) {
78+
toast.error((e as Error).message);
79+
}
80+
}
7181
</script>
7282

7383
<Sheet.Root bind:open={show}>
@@ -94,7 +104,7 @@
94104
id="name"
95105
type="text"
96106
class="w-full"
97-
value={formData.name}
107+
value={selectedFeed.name}
98108
on:input={(e) => {
99109
// two-way bind not works, so do this. https://stackoverflow.com/questions/60825553/svelte-input-binding-breaks-when-a-reactive-value-is-a-reference-type
100110
if (e.target instanceof HTMLInputElement) {
@@ -110,7 +120,7 @@
110120
id="link"
111121
type="text"
112122
class="w-full"
113-
value={formData.link}
123+
value={selectedFeed.link}
114124
on:input={(e) => {
115125
if (e.target instanceof HTMLInputElement) {
116126
formData.link = e.target.value;
@@ -126,10 +136,10 @@
126136
items={groups.map((v) => {
127137
return { value: v.id, label: v.name };
128138
})}
129-
onSelectedChange={(v) => v && (formData.group.id = v.value)}
139+
onSelectedChange={(v) => v && (formData.group_id = v.value)}
130140
>
131141
<Select.Trigger>
132-
<Select.Value placeholder={formData.group.name} />
142+
<Select.Value placeholder={selectedFeed.group.name} />
133143
</Select.Trigger>
134144
<Select.Content>
135145
{#each groups as g}
@@ -142,15 +152,49 @@
142152
</form>
143153
{/if}
144154

145-
<Separator class="my-10" />
155+
<Separator class="my-6" />
156+
<Button
157+
variant="secondary"
158+
on:click={handleRefresh}
159+
disabled={refreshing || selectedFeed.suspended}
160+
>
161+
{#if refreshing}
162+
<Loader2Icon class="mr-2 h-4 w-4 animate-spin" />
163+
{:else}
164+
Refresh
165+
{/if}
166+
</Button>
167+
168+
<Separator class="my-6" />
146169
<div class="flex flex-col w-full gap-4">
147-
<Button variant="secondary" on:click={handleRefresh} disabled={refreshing}>
148-
{#if refreshing}
149-
<Loader2Icon class="mr-2 h-4 w-4 animate-spin" />
150-
{:else}
151-
Refresh
152-
{/if}
153-
</Button>
170+
<AlertDialog.Root>
171+
<AlertDialog.Trigger asChild let:builder>
172+
<Button builders={[builder]} variant="secondary">
173+
{#if selectedFeed.suspended}
174+
Resume
175+
{:else}
176+
Suspend
177+
{/if}
178+
</Button>
179+
</AlertDialog.Trigger>
180+
<AlertDialog.Content>
181+
<AlertDialog.Header>
182+
<AlertDialog.Title>Are you absolutely sure?</AlertDialog.Title>
183+
<AlertDialog.Description>
184+
{#if selectedFeed.suspended}
185+
This will resume the refreshing of <b>{selectedFeed.name}</b>.
186+
{:else}
187+
This will suspend the refreshing of <b>{selectedFeed.name}</b> untill you resume it.
188+
{/if}
189+
</AlertDialog.Description>
190+
</AlertDialog.Header>
191+
<AlertDialog.Footer>
192+
<AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
193+
<AlertDialog.Action on:click={handleToggleSuspended}>Continue</AlertDialog.Action>
194+
</AlertDialog.Footer>
195+
</AlertDialog.Content>
196+
</AlertDialog.Root>
197+
154198
<AlertDialog.Root>
155199
<AlertDialog.Trigger asChild let:builder>
156200
<Button builders={[builder]} variant="destructive">Delete</Button>

model/feed.go

+10-1
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,17 @@ type Feed struct {
1818
LastBuild *time.Time `gorm:"last_build"`
1919
// Failure is the reason of failure. If it is not null or empty, the fetch processor
2020
// should skip this feed
21-
Failure *string `gorm:"failure;default:''"`
21+
Failure *string `gorm:"failure;default:''"`
22+
Suspended *bool `gorm:"suspended;default:false"`
2223

2324
GroupID uint
2425
Group Group
2526
}
27+
28+
func (f Feed) IsFailed() bool {
29+
return f.Failure != nil && *f.Failure != ""
30+
}
31+
32+
func (f Feed) IsSuspended() bool {
33+
return f.Suspended != nil && *f.Suspended
34+
}

repo/repo.go

+4
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,8 @@ func Init() {
5656
FirstOrCreate(&model.Group{ID: 1, Name: &defaultGroup}).Error; err != nil {
5757
panic(err)
5858
}
59+
60+
if conf.Debug {
61+
DB = DB.Debug()
62+
}
5963
}

server/feed.go

+10-7
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ func (f Feed) All() (*RespFeedAll, error) {
5454
Name: v.Name,
5555
Link: v.Link,
5656
Failure: v.Failure,
57+
Suspended: v.Suspended,
5758
UpdatedAt: v.UpdatedAt,
5859
Group: GroupForm{ID: v.GroupID, Name: v.Group.Name},
5960
})
@@ -70,11 +71,12 @@ func (f Feed) Get(req *ReqFeedGet) (*RespFeedGet, error) {
7071
}
7172

7273
return &RespFeedGet{
73-
ID: data.ID,
74-
Name: data.Name,
75-
Link: data.Link,
76-
Failure: data.Failure,
77-
Group: GroupForm{ID: data.GroupID, Name: data.Group.Name},
74+
ID: data.ID,
75+
Name: data.Name,
76+
Link: data.Link,
77+
Failure: data.Failure,
78+
Suspended: data.Suspended,
79+
Group: GroupForm{ID: data.GroupID, Name: data.Group.Name},
7880
}, nil
7981
}
8082

@@ -154,8 +156,9 @@ func (f Feed) CheckValidity(req *ReqFeedCheckValidity) (*RespFeedCheckValidity,
154156

155157
func (f Feed) Update(req *ReqFeedUpdate) error {
156158
data := &model.Feed{
157-
Name: req.Name,
158-
Link: req.Link,
159+
Name: req.Name,
160+
Link: req.Link,
161+
Suspended: req.Suspended,
159162
}
160163
if req.GroupID != nil {
161164
data.GroupID = *req.GroupID

server/feed_form.go

+6-4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ type FeedForm struct {
77
Name *string `json:"name"`
88
Link *string `json:"link"`
99
Failure *string `json:"failure"`
10+
Suspended *bool `json:"suspended"`
1011
UpdatedAt time.Time `json:"updated_at"`
1112
Group GroupForm `json:"group"`
1213
}
@@ -43,10 +44,11 @@ type ReqFeedCreate struct {
4344
}
4445

4546
type ReqFeedUpdate struct {
46-
ID uint `param:"id" validate:"required"`
47-
Name *string `json:"name"`
48-
Link *string `json:"link"`
49-
GroupID *uint `json:"group_id"`
47+
ID uint `param:"id" validate:"required"`
48+
Name *string `json:"name"`
49+
Link *string `json:"link"`
50+
Suspended *bool `json:"suspended"`
51+
GroupID *uint `json:"group_id"`
5052
}
5153

5254
type ReqFeedDelete struct {

service/pull/pull.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ func (p *Puller) PullAll(ctx context.Context) error {
7272
defer close(routinePool)
7373
wg := sync.WaitGroup{}
7474
for _, f := range feeds {
75-
if f.Failure == nil || *f.Failure != "" {
76-
// skip failed and unresolved feeds
75+
if f.IsSuspended() || f.IsFailed() {
76+
log.Printf("skip %d\n", f.ID)
7777
continue
7878
}
7979

0 commit comments

Comments
 (0)