The first commit, the core and all files is adding to repo

This commit is contained in:
2026-10-04 14:20:13 +05:00
commit 53d7b263c4
94 changed files with 10668 additions and 0 deletions

View File

@@ -0,0 +1 @@
v2.119.0

View File

@@ -0,0 +1,117 @@
create table public.profiles (id uuid primary key references auth.users(id) on delete cascade, display_name text not null default 'Explorer', role text not null default 'contributor' check(role in ('contributor','moderator')), created_at timestamptz not null default now());
create table public.catalog_servers (id uuid primary key default gen_random_uuid(), name text not null, address text, created_at timestamptz not null default now());
create table public.datasets (id uuid primary key default gen_random_uuid(), server_id uuid references public.catalog_servers(id), owner_id uuid not null references auth.users(id), name text not null, description text not null default '', source text not null check(source in ('plugin','fabric')), state text not null default 'uploading' check(state in ('uploading','processing','draft','review','published','rejected','unpublished','failed')), bounds jsonb not null, map_path text, cover_path text, panorama_count integer not null default 0, revision_id uuid, rejection_reason text, created_at timestamptz not null default now(), updated_at timestamptz not null default now());
create table public.dataset_inputs (dataset_id uuid primary key references public.datasets(id) on delete cascade, manifest jsonb not null, validated boolean not null default false);
create table public.revisions (id uuid primary key default gen_random_uuid(), dataset_id uuid not null references public.datasets(id), manifest jsonb not null, asset_root text not null, created_at timestamptz not null default now());
alter table public.datasets add constraint datasets_revision_fk foreign key(revision_id) references public.revisions(id);
create table public.upload_jobs (id uuid primary key default gen_random_uuid(), dataset_id uuid unique not null references public.datasets(id) on delete cascade, state text not null default 'queued' check(state in ('queued','running','complete','failed','cancelled')), progress integer not null default 0 check(progress between 0 and 100), attempts integer not null default 0, lease_until timestamptz, lease_owner text, error text, created_at timestamptz not null default now(), updated_at timestamptz not null default now());
create index upload_jobs_claim on public.upload_jobs(state,lease_until,created_at);
create table public.contributor_tokens (id uuid primary key default gen_random_uuid(), owner_id uuid not null references auth.users(id) on delete cascade, token_hash text unique not null, label text not null, revoked boolean not null default false, created_at timestamptz not null default now());
create table public.pairing_codes (device_hash text primary key, user_code text unique not null, owner_id uuid references auth.users(id), expires_at timestamptz not null, token_value text, polled_at timestamptz, consumed boolean not null default false);
create table public.review_decisions (id uuid primary key default gen_random_uuid(), dataset_id uuid not null references public.datasets(id), reviewer_id uuid not null references auth.users(id), decision text not null, reason text, created_at timestamptz not null default now());
create table public.reports (id uuid primary key default gen_random_uuid(), dataset_id uuid not null references public.datasets(id), owner_id uuid references auth.users(id), reason text not null, state text not null default 'open', created_at timestamptz not null default now());
create table public.game_sessions (id uuid primary key default gen_random_uuid(), revision_id uuid not null references public.revisions(id), owner_id uuid references auth.users(id) on delete set null, access_hash text not null, difficulty text not null check(difficulty in ('easy','normal','hard')), current_round integer not null default 0, created_at timestamptz not null default now());
create table public.game_rounds (session_id uuid not null references public.game_sessions(id) on delete cascade, round_number integer not null check(round_number between 0 and 4), viewpoint jsonb not null, deadline timestamptz, result jsonb, primary key(session_id,round_number));
create table public.rate_limits (key text primary key, count integer not null default 1, expires_at timestamptz not null);
alter table public.profiles enable row level security;
alter table public.catalog_servers enable row level security;
alter table public.datasets enable row level security;
alter table public.dataset_inputs enable row level security;
alter table public.revisions enable row level security;
alter table public.upload_jobs enable row level security;
alter table public.contributor_tokens enable row level security;
alter table public.pairing_codes enable row level security;
alter table public.review_decisions enable row level security;
alter table public.reports enable row level security;
alter table public.game_sessions enable row level security;
alter table public.game_rounds enable row level security;
alter table public.rate_limits enable row level security;
revoke all on all tables in schema public from anon,authenticated;
grant select on public.catalog_servers,public.datasets to anon,authenticated;
grant select on public.profiles,public.upload_jobs to authenticated;
grant all on all tables in schema public to service_role;
create policy catalog_public on public.catalog_servers for select to anon,authenticated using(exists(select 1 from public.datasets where server_id=catalog_servers.id and state='published'));
create policy datasets_visible on public.datasets for select to anon,authenticated using(state='published' or owner_id=(select auth.uid()));
create policy own_profile on public.profiles for select to authenticated using(id=(select auth.uid()));
create policy own_jobs on public.upload_jobs for select to authenticated using(exists(select 1 from public.datasets where id=dataset_id and owner_id=(select auth.uid())));
create function public.create_game(p_dataset uuid,p_difficulty text,p_owner uuid,p_access_hash text) returns uuid language plpgsql security invoker set search_path='' as $$
declare sid uuid; rev public.revisions; vp jsonb; n integer:=0;
begin
if p_difficulty not in ('easy','normal','hard') then raise exception 'Invalid difficulty'; end if;
select r.* into rev from public.datasets d join public.revisions r on r.id=d.revision_id where d.id=p_dataset and d.state='published';
if not found then raise exception 'Map unavailable'; end if;
if jsonb_array_length(rev.manifest->'viewpoints')<5 then raise exception 'Map has fewer than five viewpoints'; end if;
insert into public.game_sessions(revision_id,owner_id,access_hash,difficulty) values(rev.id,p_owner,p_access_hash,p_difficulty) returning id into sid;
for vp in select value from jsonb_array_elements(rev.manifest->'viewpoints') order by random() limit 5 loop
insert into public.game_rounds(session_id,round_number,viewpoint) values(sid,n,vp); n:=n+1; end loop;
return sid;
end; $$;
create function public.activate_round(p_session uuid,p_round integer,p_access_hash text) returns timestamptz language plpgsql security invoker set search_path='' as $$
declare s public.game_sessions; r public.game_rounds; seconds integer;
begin
select * into s from public.game_sessions where id=p_session and access_hash=p_access_hash for update;
if not found then raise exception 'Session not found'; end if;
if s.current_round<>p_round then raise exception 'Round is not current'; end if;
select * into r from public.game_rounds where session_id=p_session and round_number=p_round for update;
if r.result is not null then raise exception 'Round already finished'; end if;
if r.deadline is not null then return r.deadline; end if;
seconds:=case s.difficulty when 'easy' then 180 when 'normal' then 90 else 30 end;
update public.game_rounds set deadline=clock_timestamp()+make_interval(secs=>seconds) where session_id=p_session and round_number=p_round returning deadline into r.deadline;
return r.deadline;
end; $$;
create function public.submit_guess(p_session uuid,p_round integer,p_access_hash text,p_x double precision default null,p_z double precision default null) returns jsonb language plpgsql security invoker set search_path='' as $$
declare s public.game_sessions; r public.game_rounds; manifest jsonb; b jsonb; d double precision; diagonal double precision; points integer:=0; timed_out boolean; answer jsonb;
begin
select * into s from public.game_sessions where id=p_session and access_hash=p_access_hash for update;
if not found then raise exception 'Session not found'; end if;
select * into r from public.game_rounds where session_id=p_session and round_number=p_round for update;
if r.result is not null then return r.result; end if;
if s.current_round<>p_round or r.deadline is null then raise exception 'Round is not active'; end if;
select revisions.manifest into manifest from public.revisions where id=s.revision_id; b:=manifest->'bounds';
timed_out:=clock_timestamp()>=r.deadline;
if not timed_out and (p_x is null or p_z is null) then raise exception 'Place a guess first'; end if;
if not timed_out then
if p_x='NaN'::float8 or p_z='NaN'::float8 or p_x<(b->>'minX')::float8 or p_x>(b->>'maxX')::float8 or p_z<(b->>'minZ')::float8 or p_z>(b->>'maxZ')::float8 then raise exception 'Guess outside map'; end if;
d:=sqrt(power(p_x-(r.viewpoint->>'x')::float8,2)+power(p_z-(r.viewpoint->>'z')::float8,2));
diagonal:=sqrt(power((b->>'maxX')::float8-(b->>'minX')::float8,2)+power((b->>'maxZ')::float8-(b->>'minZ')::float8,2));
points:=case when d<=1 then 5000 else round(5000*exp(-5*d/diagonal))::integer end;
end if;
answer:=jsonb_build_object('score',points,'distance',d,'timedOut',timed_out,'actual',jsonb_build_object('x',r.viewpoint->'x','z',r.viewpoint->'z'),'guess',case when timed_out then null else jsonb_build_object('x',p_x,'z',p_z) end);
update public.game_rounds set result=answer where session_id=p_session and round_number=p_round;
update public.game_sessions set current_round=current_round+1 where id=p_session;
return answer;
end; $$;
create function public.claim_job(p_worker text) returns setof public.upload_jobs language sql security invoker set search_path='' as $$
update public.upload_jobs set state='running',attempts=attempts+1,lease_owner=p_worker,lease_until=now()+interval '2 minutes',updated_at=now() where id=(select id from public.upload_jobs where (state='queued' or (state='running' and lease_until<now())) and attempts<3 order by created_at for update skip locked limit 1) returning *;
$$;
create function public.check_rate(p_key text,p_limit integer,p_seconds integer) returns boolean language plpgsql security invoker set search_path='' as $$
declare value integer; begin
insert into public.rate_limits(key,count,expires_at) values(p_key,1,now()+make_interval(secs=>p_seconds)) on conflict(key) do update set count=case when rate_limits.expires_at<now() then 1 else rate_limits.count+1 end,expires_at=case when rate_limits.expires_at<now() then now()+make_interval(secs=>p_seconds) else rate_limits.expires_at end returning count into value;
return value<=p_limit; end; $$;
create function public.publish_dataset(p_dataset uuid,p_reviewer uuid,p_decision text,p_reason text default null) returns void language plpgsql security invoker set search_path='' as $$
declare d public.datasets; manifest jsonb; rev uuid; begin
if not exists(select 1 from public.profiles where id=p_reviewer and role='moderator') then raise exception 'Moderator permission required'; end if;
select * into d from public.datasets where id=p_dataset for update;
if not found then raise exception 'Dataset not found'; end if;
if p_decision='approve' then
if d.state<>'review' or d.panorama_count<5 then raise exception 'Map is not ready for publication'; end if;
select i.manifest into manifest from public.dataset_inputs i where dataset_id=p_dataset and validated;
if manifest is null then raise exception 'Dataset not validated'; end if;
insert into public.revisions(dataset_id,manifest,asset_root) values(p_dataset,manifest,p_dataset::text) returning id into rev;
update public.datasets set state='published',revision_id=rev,rejection_reason=null,updated_at=now() where id=p_dataset;
elsif p_decision='reject' and d.state='review' then update public.datasets set state='rejected',rejection_reason=p_reason,updated_at=now() where id=p_dataset;
elsif p_decision='unpublish' and d.state='published' then update public.datasets set state='unpublished',updated_at=now() where id=p_dataset;
else raise exception 'Invalid review transition'; end if;
insert into public.review_decisions(dataset_id,reviewer_id,decision,reason) values(p_dataset,p_reviewer,p_decision,p_reason);
end; $$;
revoke execute on all functions in schema public from public,anon,authenticated;
grant execute on all functions in schema public to service_role;
insert into storage.buckets(id,name,public,file_size_limit) values('minegeo-raw','minegeo-raw',false,52428800),('minegeo-media','minegeo-media',false,52428800) on conflict(id) do nothing;
-- Both buckets have no client policies. Uploads use bounded signed authorization;
-- reads use short-lived URLs issued only after application authorization.

View File

@@ -0,0 +1,35 @@
-- Queue transitions commit together, so a restarted worker cannot strand a draft.
create function public.finalize_upload(p_dataset uuid,p_owner uuid) returns text language plpgsql security invoker set search_path='' as $$
declare d public.datasets; begin
select * into d from public.datasets where id=p_dataset and owner_id=p_owner for update;
if not found then raise exception 'Dataset not found'; end if;
if d.state in ('processing','draft','review','published') then return d.state; end if;
if d.state<>'uploading' then raise exception 'Dataset cannot be finalized'; end if;
insert into public.upload_jobs(dataset_id) values(p_dataset) on conflict(dataset_id) do nothing;
update public.datasets set state='processing',updated_at=now() where id=p_dataset;
return 'processing'; end; $$;
create function public.complete_job(p_job uuid,p_worker text,p_count integer,p_cover text) returns void language plpgsql security invoker set search_path='' as $$
declare j public.upload_jobs; begin
select * into j from public.upload_jobs where id=p_job and lease_owner=p_worker and state='running' and lease_until>now() for update;
if not found then raise exception 'Worker lease lost'; end if;
if p_count<5 then raise exception 'Five locations required'; end if;
update public.datasets set state='draft',panorama_count=p_count,map_path=j.dataset_id::text||'/map.webp',cover_path=j.dataset_id::text||'/'||p_cover||'/preview.webp',updated_at=now() where id=j.dataset_id and state='processing';
if not found then raise exception 'Dataset processing cancelled'; end if;
update public.dataset_inputs set validated=true where dataset_id=j.dataset_id;
update public.upload_jobs set state='complete',progress=100,lease_until=null,error=null,updated_at=now() where id=p_job;
end; $$;
create function public.approve_pairing(p_code text,p_owner uuid,p_token text,p_hash text) returns void language plpgsql security invoker set search_path='' as $$
begin
update public.pairing_codes set owner_id=p_owner,token_value=p_token where user_code=p_code and owner_id is null and expires_at>now();
if not found then raise exception 'Pairing code expired or already approved'; end if;
insert into public.contributor_tokens(owner_id,token_hash,label) values(p_owner,p_hash,'Fabric capture client');
end; $$;
create function public.prevent_revision_mutation() returns trigger language plpgsql security invoker set search_path='' as $$
begin raise exception 'Published revisions are immutable'; end; $$;
create trigger revisions_immutable before update or delete on public.revisions for each row execute function public.prevent_revision_mutation();
create index game_sessions_owner_created on public.game_sessions(owner_id,created_at desc);
create index datasets_owner_created on public.datasets(owner_id,created_at desc);
create index datasets_catalog on public.datasets(state,server_id);
create index reports_open on public.reports(state,created_at);
revoke execute on function public.finalize_upload(uuid,uuid),public.complete_job(uuid,text,integer,text),public.approve_pairing(text,uuid,text,text),public.prevent_revision_mutation() from public,anon,authenticated;
grant execute on function public.finalize_upload(uuid,uuid),public.complete_job(uuid,text,integer,text),public.approve_pairing(text,uuid,text,text) to service_role;